├── .gitignore
├── LICENSE
├── README.md
├── api_keys.ini.example
├── geOSINT.py
└── requirements.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | # HTML files
92 | *.html
93 |
94 | # API keys config
95 | api_keys.ini
96 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Brandan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # geOSINT
2 | Search physical locations for geo tagged photos
3 |
4 |
5 | ## Summary ##
6 | geOSINT is a script that searches for geotagged photos on social media and plots them on a map. This can be used to perform OSINT on a physical location. If an image is found, a red marker is placed on the map. By clicking on this marker you can view the identified image.
7 |
8 | 
9 |
10 | 
11 |
12 |
13 | ## Quick Start ##
14 | $ git clone https://github.com/coldfusion39/geOSINT.git
15 | $ cd geOSINT/
16 | $ sudo pip install -r requirements.txt
17 | $ cp api_keys.ini.example api_keys.ini
18 | # add api keys
19 | $ ./geOSINT.py -a 9231 W 87th Pl -c Arvada -s CO -d 1000
20 |
21 |
22 | ## Requirements ##
23 | Run `pip install -r requirements.txt` to install the required python modules.
24 | * [Folium](https://github.com/python-visualization/folium)
25 | * [geopy](https://github.com/geopy/geopy)
26 | * [Twython](https://github.com/ryanmcgrath/twython)
27 |
28 |
29 | ## API ##
30 | geOSINT uses [FourSquare](https://developer.foursquare.com), [Flickr](https://developer.foursquare.com), and [Twitter](https://dev.twitter.com) APIs to search for photos posted within a certain distance of the supplied address. At least one API key is required required for geOSINT to return any results.
31 |
32 | Optionally, if you want to use an aerial map, similar to Google Earth, a [Mapbox](https://www.mapbox.com/studio/signup/?plan=starter) API is required.
33 |
34 | After getting your API keys, copy the `api_keys.ini.example` file to `api_keys.ini` and add your keys as shown below.
35 | ```
36 | [Mapbox]
37 | access_token: xxxxx
38 |
39 | [FourSquare]
40 | client_id: xxxxx
41 | client_secret: xxxxx
42 |
43 | [Flickr]
44 | api_key: xxxxx
45 |
46 | [Twitter]
47 | app_key: xxxxx
48 | app_secret: xxxxx
49 | oauth_token: xxxxx
50 | oauth_token_secret: xxxxx
51 | ```
52 |
53 | ## Usage ##
54 | After setting your API keys in the `api_keys.ini` file, supply geOSINT with a physical address. Optionally, you can specify the distance from the location you want to search, default is 500 meters.
55 |
56 | Example:
57 | `./geOSINT.py -a 9231 W 87th Pl -c Arvada -s CO -d 1000`
58 |
59 | Options:
60 | ```
61 | -h, --help show this help message and exit
62 | -a ADDRESS Address
63 | -c CITY City
64 | -s STATE State (ex. OH)
65 | -o OUTPUT Name of output file, (default: geo_osint.html)
66 | -d DISTANCE Distance, in meters, to search from address, (default: 500)
67 | ```
68 |
--------------------------------------------------------------------------------
/api_keys.ini.example:
--------------------------------------------------------------------------------
1 | [Mapbox]
2 | access_token:
3 |
4 | [FourSquare]
5 | client_id:
6 | client_secret:
7 |
8 | [Flickr]
9 | api_key:
10 |
11 | [Twitter]
12 | app_key:
13 | app_secret:
14 | oauth_token:
15 | oauth_token_secret:
16 |
--------------------------------------------------------------------------------
/geOSINT.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (c) 2016, Brandan Geise [coldfusion]
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 | import argparse
22 | import configparser
23 | import folium
24 | import json
25 | import os
26 | import requests
27 | from geopy.geocoders import GoogleV3
28 | from geopy.distance import vincenty
29 | from twython import Twython
30 |
31 |
32 | class GenericError(Exception):
33 | def __init__(self, *args, **kwargs):
34 | Exception.__init__(self, *args, **kwargs)
35 |
36 |
37 | def main():
38 | parser = argparse.ArgumentParser(description='Search physical locations for geotagged photos')
39 | parser.add_argument('-a', dest='address', help='Address', nargs='+', required=True)
40 | parser.add_argument('-c', dest='city', help='City', nargs='+', required=True)
41 | parser.add_argument('-s', dest='state', help='State (ex. OH)', required=True)
42 | parser.add_argument('-o', dest='output', help='Name of output file', default='geo_osint.html')
43 | parser.add_argument('-d', dest='distance', help='Distance, in meters, to search from address (default: 500)', type=int, default=500)
44 | args = parser.parse_args()
45 |
46 | # Parse config
47 | config = configparser.ConfigParser()
48 | config.read(os.path.abspath('api_keys.ini'))
49 |
50 | # Get coordinates
51 | lat, lon, address = get_coords(' '.join(args.address), ' '.join(args.city), args.state)
52 |
53 | print_good("Looking for images within {0} meters of {1}".format(args.distance, address))
54 |
55 | if config.get('Mapbox', 'access_token'):
56 | tiles = "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/{{z}}/{{x}}/{{y}}?access_token={0}".format(config.get('Mapbox', 'access_token'))
57 | attr = 'Mapbox attribution'
58 | else:
59 | tiles = 'Stamen Toner'
60 | attr = ''
61 |
62 | # Setup map
63 | maps = folium.Map(
64 | location=[lat, lon],
65 | tiles=tiles,
66 | attr=attr,
67 | zoom_start=16
68 | )
69 |
70 | folium.CircleMarker(
71 | location=[lat, lon],
72 | radius=args.distance,
73 | color='#3186cc',
74 | fill_color='#3186cc',
75 | fill_opacity=0.2
76 | ).add_to(maps)
77 |
78 | # Get FourSquare photos
79 | if config.get('FourSquare', 'client_id') and config.get('FourSquare', 'client_secret'):
80 | print_status('Getting images from FourSquare...')
81 | maps = get_foursquare_venues(config.get('FourSquare', 'client_id'), config.get('FourSquare', 'client_secret'), lat, lon, args.distance, maps)
82 | else:
83 | print_warn('No Foursquare API keys in config')
84 |
85 | # Get Flickr photos
86 | if config.get('Flickr', 'api_key'):
87 | print_status('Getting images from Flickr...')
88 | maps = get_flickr_photos(config.get('Flickr', 'api_key'), lat, lon, args.distance, maps)
89 | else:
90 | print_warn('No Flickr API keys in config')
91 |
92 | # Get Twitter photos
93 | if config.get('Twitter', 'app_key') and config.get('Twitter', 'app_secret') and config.get('Twitter', 'oauth_token') and config.get('Twitter', 'oauth_token_secret'):
94 | print_status('Getting images from Twitter...')
95 | maps = get_twitter_photos(config.get('Twitter', 'app_key'), config.get('Twitter', 'app_secret'), config.get('Twitter', 'oauth_token'), config.get('Twitter', 'oauth_token_secret'), lat, lon, args.distance, maps)
96 | else:
97 | print_warn('No Twitter API keys in config')
98 |
99 | maps.save(args.output)
100 | print_good("Outfile written to {0}".format(os.path.abspath(args.output)))
101 |
102 |
103 | # Get Geo-location coordinates
104 | def get_coords(address, city, state):
105 | try:
106 | geolocator = GoogleV3(timeout=5)
107 | address = "{0}, {1}, {2}".format(address, city, state)
108 | location = geolocator.geocode(address, exactly_one=True)
109 | lat = location.latitude
110 | lon = location.longitude
111 | except Exception as error:
112 | raise GenericError(error)
113 |
114 | return lat, lon, address
115 |
116 |
117 | # Get venue IDs
118 | def get_foursquare_venues(key, secret, lat, lon, radius, maps):
119 | venue_url = "https://api.foursquare.com/v2/venues/search?ll={0},{1}&limit=50&radius={2}&client_id={3}&client_secret={4}&v=20130815".format(lat, lon, radius, key, secret)
120 | response = requests.get(venue_url)
121 | if response.status_code == 200:
122 | search_results = json.loads(response.text)
123 | for result in search_results['response']['venues']:
124 | venue_id = result['id']
125 | venue_name = result['name']
126 | try:
127 | photo_lat = result['location']['labeledLatLngs'][0]['lat']
128 | photo_lon = result['location']['labeledLatLngs'][0]['lng']
129 | except KeyError:
130 | photo_lat = result['location']['lat']
131 | photo_lon = result['location']['lng']
132 |
133 | distance = vincenty((lat, lon), (photo_lat, photo_lon)).meters
134 | if int(distance) <= radius:
135 | maps = get_foursquare_photos(venue_name, venue_id, key, secret, photo_lat, photo_lon, maps)
136 | else:
137 | raise GenericError('Could not connect to FourSquare')
138 |
139 | return maps
140 |
141 |
142 | # Get photo URLs
143 | def get_foursquare_photos(name, venue, key, secret, lat, lon, maps):
144 | photos = []
145 | photo_url = "https://api.foursquare.com/v2/venues/{0}/photos?limit=200&offset=1&client_id={1}&client_secret={2}&v=20130815".format(venue, key, secret)
146 | response = requests.get(photo_url)
147 | if response.status_code == 200:
148 | search_results = json.loads(response.text)
149 | if search_results['response']['photos']['count'] > 0:
150 | for result in search_results['response']['photos']['items']:
151 | photo = "{0}original{1}".format(result['prefix'], result['suffix'])
152 | photos.append(photo)
153 | iframe = get_frame(photos)
154 |
155 | folium.CircleMarker(
156 | location=[lat, lon],
157 | radius=3,
158 | popup=folium.Popup(iframe, max_width=2650),
159 | color='Red',
160 | fill_opacity=1.0,
161 | fill_color='Red'
162 | ).add_to(maps)
163 | else:
164 | raise GenericError('Could not connect to FourSquare')
165 |
166 | return maps
167 |
168 |
169 | # Get photos from Flickr
170 | def get_flickr_photos(key, lat, lon, radius, maps):
171 | photos = []
172 | api_url = "https://api.flickr.com/services/rest/?method=flickr.photos.search&format=json&accuracy=16&content_type=4&lat={0}&lon={1}&radius={2}&per_page=500&page=1&api_key={3}".format(lat, lon, (float(radius) / 1000.0), key)
173 | response = requests.get(api_url)
174 | if response.status_code == 200:
175 | search_results = json.loads((response.content).replace('jsonFlickrApi(', '').replace(')', ''))
176 | for result in search_results['photos']['photo']:
177 | photo_id = result['id']
178 | photo_farm = result['farm']
179 | photo_server = result['server']
180 | photo_secret = result['secret']
181 |
182 | photo_lat, photo_lon = flickr_photo_coords(key, photo_id)
183 |
184 | distance = vincenty((lat, lon), (photo_lat, photo_lon)).meters
185 | if int(distance) <= radius:
186 | photo = ["https://c2.staticflickr.com/{0}/{1}/{2}_{3}_b.jpg".format(photo_farm, photo_server, photo_id, photo_secret)]
187 | iframe = get_frame(photo)
188 |
189 | folium.CircleMarker(
190 | location=[photo_lat, photo_lon],
191 | radius=3,
192 | popup=folium.Popup(iframe, max_width=2650),
193 | color='Red',
194 | fill_opacity=1.0,
195 | fill_color='Red'
196 | ).add_to(maps)
197 | else:
198 | raise GenericError('Could not connect to Flickr')
199 |
200 | return maps
201 |
202 |
203 | # Get Flickr photo coordinates
204 | def flickr_photo_coords(key, photo):
205 | exif_url = "https://api.flickr.com/services/rest/?method=flickr.photos.geo.getLocation&photo_id={0}&format=json&api_key={1}".format(photo, key)
206 | response = requests.get(exif_url)
207 | if response.status_code == 200:
208 | search_results = json.loads((response.content).replace('jsonFlickrApi(', '').replace(')', ''))
209 | lat = search_results['photo']['location']['latitude']
210 | lon = search_results['photo']['location']['longitude']
211 | else:
212 | raise GenericError('Could not get Flickr photo')
213 |
214 | return lat, lon
215 |
216 |
217 | # Get photos from Twitter
218 | def get_twitter_photos(app_key, app_secret, oauth_token, oauth_token_secret, lat, lon, radius, maps):
219 | twitter = Twython(app_key, app_secret, oauth_token, oauth_token_secret)
220 | twitter.verify_credentials()
221 |
222 | query = "geocode:{0},{1},{2}km -RT".format(lat, lon, float(radius) / 1000.0)
223 | results = twitter.search(q=query, count=100)
224 | for tweet in results['statuses']:
225 | if tweet.get('geo') is None:
226 | continue
227 | photo_lat, photo_lon = tweet['geo']['coordinates']
228 | media_url = next((e[0]['media_url'] for e in tweet['entities'] if 'media' in e), None)
229 | if media_url is None:
230 | continue
231 | photos = [media_url]
232 | distance = vincenty((lat, lon), (photo_lat, photo_lon)).meters
233 | if int(distance) <= radius:
234 | iframe = get_frame(photos)
235 |
236 | folium.CircleMarker(
237 | location=[photo_lat, photo_lon],
238 | radius=3,
239 | popup=folium.Popup(iframe, max_width=2650),
240 | color='Red',
241 | fill_opacity=1.0,
242 | fill_color='Red'
243 | ).add_to(maps)
244 |
245 | return maps
246 |
247 |
248 | # Create iFrame
249 | def get_frame(urls):
250 | html = ''
251 | for url in urls:
252 | html += "