├── .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 | ![Demo 1](https://raw.githubusercontent.com/coldfusion39/geOSINT/screenshots/demo1.png) 9 | 10 | ![Demo 2](https://raw.githubusercontent.com/coldfusion39/geOSINT/screenshots/demo2.png) 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 += "
".format(url) 253 | 254 | iframe = folium.element.IFrame(html=html, width=250, height=300) 255 | 256 | return iframe 257 | 258 | 259 | def print_error(msg): 260 | print "\033[1m\033[31m[-]\033[0m {0}".format(msg) 261 | 262 | 263 | def print_status(msg): 264 | print "\033[1m\033[34m[*]\033[0m {0}".format(msg) 265 | 266 | 267 | def print_good(msg): 268 | print "\033[1m\033[32m[+]\033[0m {0}".format(msg) 269 | 270 | 271 | def print_warn(msg): 272 | print "\033[1m\033[33m[!]\033[0m {0}".format(msg) 273 | 274 | 275 | if __name__ == '__main__': 276 | main() 277 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | folium 2 | geopy 3 | twython 4 | --------------------------------------------------------------------------------