Skip to content

Commit e4d9fd5

Browse files
authored
Merge pull request #2 from PacketTotal/feature/analyst_tools
removed test print statement
2 parents fd304ea + c9d163b commit e4d9fd5

File tree

6 files changed

+133
-23
lines changed

6 files changed

+133
-23
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## 0.1.0
2+
3+
- API Wrapper via the `packettotal_sdk/packettotal_api.py` wrapper module
4+
- packettotal cmdline added
5+
6+
## 0.2.0
7+
- Bug fixes:
8+
- `packettotal_sdk/packettotal_api.PacketTotalAPI.deep_search` now returns a proper `request.Response` obj
9+
10+
- Added `packettotal_sdk/packettotal_api.search_tools` module
11+
- provides the ability to search by a list of IOCs

docs/.buildinfo

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/_modules/packettotal_sdk/packettotal_api.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ <h1>Source code for packettotal_sdk.packettotal_api</h1><div class="highlight"><
8383
<span class="n">body</span><span class="p">[</span><span class="s1">&#39;pcap_name&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pcap_name</span>
8484

8585
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">pcap_sources</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span>
86-
<span class="nb">print</span><span class="p">(</span><span class="nb">type</span><span class="p">(</span><span class="n">pcap_sources</span><span class="p">),</span> <span class="n">pcap_sources</span><span class="p">)</span>
8786
<span class="n">body</span><span class="p">[</span><span class="s1">&#39;sources&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pcap_sources</span>
8887

8988
<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span>
@@ -127,7 +126,7 @@ <h1>Source code for packettotal_sdk.packettotal_api</h1><div class="highlight"><
127126
<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span>
128127
<span class="bp">self</span><span class="o">.</span><span class="n">base_url</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">version</span> <span class="o">+</span> <span class="s1">&#39;/search/deep&#39;</span><span class="p">,</span>
129128
<span class="n">headers</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">,</span>
130-
<span class="n">body</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
129+
<span class="n">data</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
131130
<span class="p">)</span>
132131

133132
<span class="k">return</span> <span class="n">response</span></div>
@@ -147,7 +146,7 @@ <h1>Source code for packettotal_sdk.packettotal_api</h1><div class="highlight"><
147146
<span class="n">headers</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">headers</span>
148147
<span class="p">)</span>
149148

150-
<span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span></div>
149+
<span class="k">return</span> <span class="n">response</span></div>
151150

152151
<div class="viewcode-block" id="PacketTotalApi.pcap_analysis"><a class="viewcode-back" href="../../packettotal_sdk/packettotal_sdk.html#packettotal_sdk.packettotal_api.PacketTotalApi.pcap_analysis">[docs]</a> <span class="k">def</span> <span class="nf">pcap_analysis</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">pcap_md5</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">requests</span><span class="o">.</span><span class="n">Response</span><span class="p">:</span>
153152
<span class="sd">&quot;&quot;&quot;</span>

packettotal_sdk/packettotal_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def deep_search_create(self, query: str) -> requests.Response:
9393
response = requests.post(
9494
self.base_url + self.version + '/search/deep',
9595
headers=self.headers,
96-
body=json.dumps(body)
96+
data=json.dumps(body)
9797
)
9898

9999
return response
@@ -113,7 +113,7 @@ def deep_search_get(self, search_id: str, pretty=False) -> requests.Response:
113113
headers=self.headers
114114
)
115115

116-
return response.text
116+
return response
117117

118118
def pcap_analysis(self, pcap_md5: str) -> requests.Response:
119119
"""

packettotal_sdk/search_tools.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import time
2+
import typing
3+
import requests
4+
from sys import stderr
5+
from datetime import datetime
6+
7+
8+
from packettotal_sdk import packettotal_api
9+
10+
11+
class SearchTools(packettotal_api.PacketTotalApi):
12+
13+
def __init__(self, api_key: str):
14+
"""
15+
:param api_key: An API authentication token
16+
"""
17+
super().__init__(api_key)
18+
19+
def search_by_pcap(self, pcap_file_obj: typing.BinaryIO) -> requests.Response:
20+
"""
21+
Search by a pcap/pcapng file, get list list of similar packet captures
22+
23+
:param pcap_file_obj: A file like object that provides a .read() interface (E.G open('path_to_pcap.pcap, 'rb') )
24+
:return: A request.Response instance, containing a graph of similar pcaps with matched terms
25+
"""
26+
response = super().analyze(pcap_file_obj)
27+
if response.status_code == 200:
28+
sim_response = super().pcap_similar(response.json()['pcap_metadata']['md5'])
29+
elif response.status_code == 202:
30+
pcap_id = response.json()['id']
31+
info_response = super().pcap_info(pcap_id)
32+
while info_response.status_code == 404:
33+
print('[{}] Waiting for {} to finish analyzing.'.format(datetime.utcnow(), pcap_id))
34+
info_response = super().pcap_info(response.json()['id'])
35+
time.sleep(10)
36+
sim_response = super().pcap_similar(response.json()['id'])
37+
else:
38+
return response
39+
return sim_response
40+
41+
def search_by_iocs(self, ioc_file: typing.TextIO) -> requests.Response:
42+
"""
43+
Search up to 1000 IOC terms at once, and get matching packet captures
44+
45+
:param ioc_file: A file like object that provides a .read() interface (E.G open('path_to_iocs.txt, 'r')
46+
contents are line delim
47+
:return: A request.Response instance containing the search results containing at least one matching IOC
48+
"""
49+
text = ioc_file.read()
50+
delim = '\n'
51+
if '\r\n' in text[0:2048]:
52+
delim = '\r\n'
53+
elif '\r' in text[0:2048]:
54+
delim = '\r'
55+
elif ',' in text[0:2048]:
56+
delim = ','
57+
elif '\t' in text[0:2048]:
58+
delim = '\t'
59+
text_delimd = text.split(delim)
60+
search_str = ''
61+
for i, ioc in enumerate(text_delimd[0: -2]):
62+
search_str += '"{}" OR '.format(ioc.strip())
63+
if i > 1000:
64+
print('Warning searching only the first 1000 IOC terms of {}.'.format(len(text_delimd)), file=stderr)
65+
break
66+
search_str += '"{}"'.format(text_delimd[-1].strip())
67+
response = super().search(search_str)
68+
return response

scripts/packettotal

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import getpass
66
import argparse
77

88
import requests
9-
from packettotal_sdk import packettotal_api
9+
from packettotal_sdk import search_tools
1010

1111

1212
def _output_text_response(response: requests.Response, output_file=None) -> None:
@@ -36,7 +36,7 @@ def _output_binary_response(response: requests.Response, pcap_id: str) -> None:
3636
:param response: A request.Response instance containing a populated .content variable
3737
:param pcap_id: An md5 hash corresponding to the pcap file submission on PacketTotal.com
3838
"""
39-
if response.status_code not in [200, 301]:
39+
if response.status_code not in [200, 303]:
4040
print('An error occurred. [{}]'.format(response.status_code), file=sys.stderr)
4141
print(response.text, file=sys.stderr)
4242
elif response.status_code == 202:
@@ -50,8 +50,8 @@ def _output_binary_response(response: requests.Response, pcap_id: str) -> None:
5050
def _parse_cmdline() -> argparse.Namespace:
5151
"""
5252
positional arguments:
53-
mode The mode to invoke [ analyze|search|deep_search
54-
|deep_search_results|info|analysis|download|similar ]
53+
mode The mode to invoke [ analyze|search|search_by_pcap|ioc_search|deep_search
54+
|deep_search_results|info|analysis|download|similar| ]
5555
5656
optional arguments:
5757
-h, --help show this help message and exit
@@ -72,8 +72,8 @@ def _parse_cmdline() -> argparse.Namespace:
7272
)
7373
parser.add_argument('mode', metavar='mode', type=str,
7474
help='The mode to invoke '
75-
'[ analyze|search|deep_search|deep_search_results|info|analysis|'
76-
'download|similar ]')
75+
'[ analyze|search|search_by_pcap|ioc_search|deep_search|deep_search_results|info|analysis|'
76+
'download|similar|]')
7777
parser.add_argument(
7878
'--query', dest='query', help='A search query',
7979
required='search' in sys.argv or 'deep_search' in sys.argv)
@@ -85,7 +85,11 @@ def _parse_cmdline() -> argparse.Namespace:
8585
required='deep_search_results' in sys.argv)
8686
parser.add_argument(
8787
'--path', dest='pcap_path', help='The path to the pcap file you wish to analyze.',
88-
required='analyze' in sys.argv)
88+
required='analyze' in sys.argv or 'search_by_pcap' in sys.argv)
89+
parser.add_argument('--ioc-path', dest='ioc_path', help='The path to a newline delimited text file containing '
90+
'search terms.', required='ioc_search' in sys.argv)
91+
parser.add_argument('--skip-warnings', dest='skip_warnings', help='Skip pcap analyze warning prompt',
92+
action='store_true')
8993
parser.add_argument('--name', dest='pcap_name', help='The optional name associated with the pcap analysis.',
9094
default=None)
9195
parser.add_argument('--sources', dest='pcap_sources', help='URLs referencing the original analysis.',
@@ -96,44 +100,68 @@ def _parse_cmdline() -> argparse.Namespace:
96100
return args
97101

98102

99-
def get_api() -> packettotal_api.PacketTotalApi:
103+
def get_api() -> search_tools.SearchTools:
100104
"""
101105
Gets an authenticated API instance either by prompting the user or retrieving credential from disk
102106
:return: Returns a validated packettotal_api.PacketTotalApi instance
103107
"""
104108
try:
105109
with open('.pt_api_key', 'r') as f:
106-
api = packettotal_api.PacketTotalApi(f.read())
110+
api = search_tools.SearchTools(f.read())
107111
if not validate_api_key(api):
108112
api = prompt_api_token()
109113
except FileNotFoundError:
110114
api = prompt_api_token()
111115
return api
112116

113117

114-
def prompt_api_token() -> packettotal_api.PacketTotalApi:
118+
def prompt_analysis_disclaimer():
119+
print("""
120+
WARNING: Analysis will result in the network traffic becoming public at https://packettotal.com.
121+
122+
ADVERTENCIA: El análisis hará que el tráfico de la red se haga público en https://packettotal.com.
123+
124+
WARNUNG: Die Analyse führt dazu, dass der Netzwerkverkehr unter https://packettotal.com öffentlich wird.
125+
126+
ПРЕДУПРЕЖДЕНИЕ. Анализ приведет к тому, что сетевой трафик станет общедоступным на https://packettotal.com.
127+
128+
चेतावनी: विश्लेषण का परिणाम नेटवर्क ट्रैफिक https://packettotal.com पर सार्वजनिक हो जाएगा
129+
130+
警告:分析将导致网络流量在https://packettotal.com上公开
131+
132+
警告:分析により、ネットワークトラフィックはhttps://packettotal.comで公開されます。
133+
134+
tahdhir: sayuadiy altahlil 'iilaa 'an tusbih harakat murur alshabakat eamat ealaa https://packettotal.com
135+
""")
136+
137+
answer = input('Continue? [Y/n]: ')
138+
if answer.lower() == 'n':
139+
exit(0)
140+
141+
142+
def prompt_api_token() -> search_tools.SearchTools:
115143
"""
116144
Prompts the user to enter their API key, tests the validity of the key, and if valid caches it to disk
117145
118-
:return: Returns a validated packettotal_api.PacketTotalApi instance
146+
:return: Returns a validated search_tools.SearchTools instance
119147
"""
120148
valid = False
121149
api = None
122150
while not valid:
123151
api_key = getpass.getpass('Enter your API key: ', stream=None)
124-
api = packettotal_api.PacketTotalApi(api_key)
152+
api = search_tools.SearchTools(api_key)
125153
if validate_api_key(api):
126154
with open('.pt_api_key', 'w') as f:
127155
f.write(api_key)
128156
valid = True
129157
return api
130158

131159

132-
def validate_api_key(api: packettotal_api.PacketTotalApi) -> bool:
160+
def validate_api_key(api: search_tools.SearchTools) -> bool:
133161
"""
134162
Determines whether an API key is valid
135163
136-
:param api: An unvalidated packettotal_api.PacketTotalApi instance
164+
:param api: An unvalidated search_tools.SearchTools instance
137165
:return: True if api key is valid
138166
"""
139167
valid = False
@@ -151,6 +179,8 @@ def main():
151179
return
152180
api = get_api()
153181
if args.mode == 'analyze':
182+
if not args.skip_warnings:
183+
prompt_analysis_disclaimer()
154184
if args.pcap_sources:
155185
pcap_sources = args.pcap_sources
156186
else:
@@ -161,6 +191,8 @@ def main():
161191
_output_text_response(api.search(args.query), args.output)
162192
elif args.mode == 'deep_search':
163193
_output_text_response(api.deep_search_create(args.query), args.output)
194+
elif args.mode == 'ioc_search':
195+
_output_text_response(api.search_by_iocs(open(args.ioc_path, 'r')), args.output)
164196
elif args.mode == 'deep_search_results':
165197
_output_text_response(api.deep_search_get(args.search_id), args.output)
166198
elif args.mode == 'info':
@@ -183,6 +215,10 @@ def main():
183215
_output_text_response(api.pcap_similar(args.pcap_id), args.output)
184216
elif args.mode == 'usage':
185217
_output_text_response(api.usage(), args.output)
218+
elif args.mode == 'search_by_pcap':
219+
if not args.skip_warnings:
220+
prompt_analysis_disclaimer()
221+
_output_text_response(api.search_by_pcap(open(args.pcap_path, 'rb')), args.output)
186222
else:
187223
print('Invalid mode. Valid modes are: [{}]'.format('analyze|search|deep_search|deep_search_results|'
188224
'info|analysis|download|similar'),

0 commit comments

Comments
 (0)