AnalysisClientBase   F
last analyzed

Complexity

Total Complexity 110

Size/Duplication

Total Lines 1290
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1290
rs 0.6314
c 0
b 0
f 0
wmc 110

31 Methods

Rating   Name   Duplication   Size   Complexity  
B rescore_task() 0 41 2
B rescore_scanner() 0 51 5
A __init__() 0 4 1
A get_backend_scores() 0 16 2
F query_file_hash() 0 40 17
A submit_url() 0 58 3
A _api_request() 0 21 1
A _process_response_page() 0 19 4
A get_detections() 0 10 1
A get_result() 0 54 2
C create_ticket() 0 61 8
A get_license_activity() 0 8 2
B query_analysis_engine_tasks() 0 24 1
B get_completed() 0 43 3
A _logger() 0 2 1
B analyze_sandbox_result() 0 39 1
A query_task_artifact() 0 15 1
A completed() 0 6 1
C get_result_artifact() 0 51 8
A __build_file_download_url() 0 10 2
A suppress_scanner() 0 15 1
B submit_exe_file() 0 34 1
A submit_file_metadata() 0 66 1
B submit_exe_hash() 0 33 1
C submit_file_hash() 0 194 7
A _check_file_like() 0 4 2
A get_progress() 0 18 1
D submit_file() 0 192 8
B get_result_summary() 0 35 2
F is_blocked_file_hash() 0 41 17
A __build_url() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like AnalysisClientBase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/python
2
# flake8: noqa
3
# pylint: skip-file
4
"""
5
This is a Python client for the Lastline Analyst API.
6
7
The :py:class:`AnalysisClient` class implements
8
the client side of the Lastline Analyst API methods.
9
It can be imported into Python client code
10
that uses the API.
11
12
The client is available at
13
https://analysis.lastline.com/docs/llapi_client/analysis_apiclient.py.
14
15
Requirements
16
+++++++++++++++++++
17
18
The Analysis API client requires:
19
20
- python 2.6 or 2.7.
21
- The python requests module.
22
- The python pycurl module.
23
- To use the client as a python shell, the ipython module.
24
25
Required python modules can be installed
26
using tools such as easy_install or pip, e.g.::
27
28
    easy_install requests
29
    pip install ipython
30
31
Analysis Client Shell
32
+++++++++++++++++++++++
33
34
Running the analysis API client from the command line,
35
it provides a shell for manually sending requests
36
to the Lastline Analyst API. This can be used to
37
try out the API by analyzing files or URLs.
38
39
This is an IPython shell, so you can take
40
advantage of tab auto-completion and other
41
convenient features of IPython.
42
43
Once the shell is started, the current context
44
contains an 'analysis' object. This is an :py:class:`AnalysisClient`,
45
which can be used to access the functionality
46
of the lastline Analysis API.
47
48
To start the shell, invoke::
49
50
    python analysis_apiclient.py API_KEY API_TOKEN
51
52
replacing API_KEY and API_TOKEN with your API credentials.
53
54
By default, the client connects to an API instance running in the Lastline cloud
55
at https://analysis.lastline.com . To connect to a different instance, for
56
example when using a Lastline On-Premise installation, please use the
57
*--api-url* parameter to point to the URL of the On-Premise API. For example, to
58
connect to a Lastline Analyst On-Premise running at *analyst.lastline.local*,
59
use::
60
61
    python analysis_apiclient.py --api-url https://analyst.lastline.local/ API_KEY API_TOKEN
62
63
"""
64
import collections
65
import datetime
66
import sys
67
import time
68
69
try:
70
    import json
71
    import StringIO
72
    import requests
73
    if __name__ == "__main__":
74
        import optparse
75
        import IPython
76
except ImportError, e:
77
    if __name__ == "__main__":
78
        print >> sys.stderr, \
79
            "A module required for running the analysis API example \
80
            shell was not found:"
81
        print >> sys.stderr, "\t'%s'" % str(e)
82
        print >> sys.stderr, "Please install the missing module."
83
        print >> sys.stderr, "For this, you can use tools such as easy_install or pip:"
84
        print >> sys.stderr, "\t easy_install <MODULE_NAME>"
85
        print >> sys.stderr, "\t pip install <MODULE_NAME>"
86
        sys.exit(1)
87
    else:
88
        raise
89
90
try:
91
    from llapi_client import get_proxies_from_config
92
except ImportError:
93
    # Non-Lastline environment. Reading from config not support/needed.
94
    get_proxies_from_config = None
95
96
try:
97
    requests_version = requests.__version__
98
    if not requests_version.startswith('2.2'):
99
        raise Exception()
100
except Exception:
101
    requests_version = '?'
102
    print >> sys.stderr, "Warning: Your version of requests (%s) might not " \
103
                         "be compatible with this module." % requests_version
104
    print >> sys.stderr, "Officially supported are versions 2.2.x"
105
106
107
# copied these values from Lastline utility code (llapi) to make them available
108
# to users of client code. please keep in sync!
109
ANALYSIS_API_FILE_NOT_AVAILABLE = 101
110
ANALYSIS_API_UNKNOWN_RESOURCE_TYPE = 102
111
ANALYSIS_API_UNKNOWN_ANALYSIS_TYPE = 103
112
ANALYSIS_API_INVALID_CREDENTIALS = 104
113
ANALYSIS_API_INVALID_UUID = 105
114
ANALYSIS_API_NO_RESULT_FOUND = 106
115
ANALYSIS_API_TEMPORARILY_UNAVAILABLE = 107
116
ANALYSIS_API_PERMISSION_DENIED = 108
117
ANALYSIS_API_FILE_TOO_LARGE = 109
118
ANALYSIS_API_INVALID_DOMAIN = 110
119
ANALYSIS_API_INVALID_D_METADATA = 112
120
ANALYSIS_API_INVALID_FILE_TYPE = 113
121
ANALYSIS_API_INVALID_ARTIFACT_UUID = 114
122
ANALYSIS_API_SUBMISSION_LIMIT_EXCEEDED = 115
123
ANALYSIS_API_INVALID_HASH_ALGORITHM = 116
124
ANALYSIS_API_INVALID_URL = 117
125
ANALYSIS_API_INVALID_REPORT_VERSION = 118
126
ANALYSIS_API_FILE_EXTRACTION_FAILED = 119
127
128
129
class Error(Exception):
130
    """
131
    Base exception class for this module.
132
    """
133
134
135
class InvalidSubApiType(Error):
136
    """
137
    Exception for invalid sub API operations.
138
139
    The analysis API consists of a number of views (sub APIs):
140
    (only analysis for now)
141
    Operations involving parts other than these will
142
    raise this exceptions.
143
    """
144
    def __init__(self, sub_api_type):
145
        Error.__init__(self)
146
        self.sub_api_type = sub_api_type
147
148
    def __str__(self):
149
        return "Invalid sub API '%s', expecting one of (%s)" % \
150
            (self.sub_api_type, ','.join(AnalysisClientBase.SUB_APIS))
151
152
153
class InvalidFormat(Error):
154
    """
155
    Invalid format requested.
156
    """
157
    def __init__(self, requested_format):
158
        Error.__init__(self)
159
        self.format = requested_format
160
161
    def __str__(self):
162
        return "Requested Invalid Format '%s', expecting one of (%s)" % \
163
            (self.format, ','.join(AnalysisClientBase.FORMATS))
164
165
166
class CommunicationError(Error):
167
    """
168
    Contacting Malscape failed.
169
    """
170
    def __init__(self, msg=None, error=None):
171
        Error.__init__(self, msg or error or '')
172
        self.__error = error
173
174
    def internal_error(self):
175
        return self.__error
176
177
178
class InvalidAnalysisAPIResponse(Error):
179
    """
180
    An AnalysisAPI response was not in the expected format
181
    """
182
183
184
class AnalysisAPIError(Error):
185
    """
186
    Analysis API returned an error.
187
188
    The `error_code` member of this exception
189
    is the :ref:`error code returned by the API<error_codes>`.
190
    """
191
    def __init__(self, msg, error_code):
192
        Error.__init__(self)
193
        self.msg = msg
194
        self.error_code = error_code
195
196
    def __str__(self):
197
        if self.error_code:
198
            return "Analysis API error (%s): %s" % (self.error_code, self.msg)
199
        return "Analysis API error: %s" % self.msg
200
201
202
class RequestError(AnalysisAPIError):
203
    """
204
    Exception class to group errors that are permanent request errors when
205
    following the "malscape protocol". These errors indicate a problem with the
206
    request sent to the server - if you repeat the same request, you cannot
207
    expect a different error.
208
209
    This group excludes temporary errors, such as authentication problems.
210
    """
211
212
213
class SubmissionInvalidError(RequestError):
214
    """
215
    Exception class to group errors that are permanent submission errors. See
216
    `RequestError` for details.
217
    """
218
219
220
class FileNotAvailableError(AnalysisAPIError):
221
    def __init__(self, msg, error_code=ANALYSIS_API_FILE_NOT_AVAILABLE):
222
        AnalysisAPIError.__init__(self, msg, error_code)
223
224
225
class InvalidCredentialsError(AnalysisAPIError):
226
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_CREDENTIALS):
227
        AnalysisAPIError.__init__(self, msg, error_code)
228
229
230
class InvalidUUIDError(RequestError):
231
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_UUID):
232
        RequestError.__init__(self, msg, error_code)
233
234
235
class NoResultFoundError(AnalysisAPIError):
236
    def __init__(self, msg, error_code=ANALYSIS_API_NO_RESULT_FOUND):
237
        AnalysisAPIError.__init__(self, msg, error_code)
238
239
240
class TemporarilyUnavailableError(AnalysisAPIError):
241
    def __init__(self, msg, error_code=ANALYSIS_API_TEMPORARILY_UNAVAILABLE):
242
        AnalysisAPIError.__init__(self, msg, error_code)
243
244
245
class PermissionDeniedError(AnalysisAPIError):
246
    def __init__(self, msg, error_code=ANALYSIS_API_PERMISSION_DENIED):
247
        AnalysisAPIError.__init__(self, msg, error_code)
248
249
250
class FileTooLargeError(SubmissionInvalidError):
251
    def __init__(self, msg, error_code=ANALYSIS_API_FILE_TOO_LARGE):
252
        SubmissionInvalidError.__init__(self, msg, error_code)
253
254
255
class InvalidFileTypeError(SubmissionInvalidError):
256
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_FILE_TYPE):
257
        SubmissionInvalidError.__init__(self, msg, error_code)
258
259
260
class InvalidMetadataError(SubmissionInvalidError):
261
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_D_METADATA):
262
        SubmissionInvalidError.__init__(self, msg, error_code)
263
264
265
class InvalidArtifactError(RequestError):
266
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_ARTIFACT_UUID):
267
        RequestError.__init__(self, msg, error_code)
268
269
270
class SubmissionLimitExceededError(AnalysisAPIError):
271
    def __init__(self, msg, error_code=ANALYSIS_API_SUBMISSION_LIMIT_EXCEEDED):
272
        AnalysisAPIError.__init__(self, msg, error_code)
273
274
275
class InvalidHashAlgorithmError(RequestError):
276
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_HASH_ALGORITHM):
277
        RequestError.__init__(self, msg, error_code)
278
279
280
class InvalidURLError(SubmissionInvalidError):
281
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_URL):
282
        SubmissionInvalidError.__init__(self, msg, error_code)
283
284
285
class InvalidReportVersionError(RequestError):
286
    def __init__(self, msg, error_code=ANALYSIS_API_INVALID_REPORT_VERSION):
287
        RequestError.__init__(self, msg, error_code)
288
289
290
class FileExtractionFailedError(SubmissionInvalidError):
291
    def __init__(self, msg, error_code=ANALYSIS_API_FILE_EXTRACTION_FAILED):
292
        SubmissionInvalidError.__init__(self, msg, error_code)
293
294
295
#################
296
# client
297
#################
298
299
__COMPLETED_TASK_FIELDS = [
300
    "task_uuid",
301
    "score"
302
]
303
CompletedTask = collections.namedtuple("CompletedTask", __COMPLETED_TASK_FIELDS)
304
305
306
def get_time():
307
    """
308
    trivial wrapper around time.time to make testing easier
309
    """
310
    return time.time()
311
312
313
def purge_none(d):
314
    """
315
    Purge None entries from a dictionary
316
    """
317
    for k in d.keys():
318
        if d[k] is None:
319
            del d[k]
320
    return d
321
322
323
def parse_datetime(d):
324
    """
325
    Parse a datetime as formatted in one of the following formats:
326
327
    date: %Y-%m-%d'
328
    datetime: '%Y-%m-%d %H:%M:%S'
329
    datetime with microseconds: '%Y-%m-%d %H:%M:%S.%f'
330
331
    Can also handle a datetime.date or datetime.datetime object,
332
    (or anything that has year, month and day attributes)
333
    and converts it to datetime.datetime
334
    """
335
    if hasattr(d, "year") and hasattr(d, "month") and hasattr(d, "day"):
336
        return datetime.datetime(d.year, d.month, d.day)
337
338
    try:
339
        return datetime.datetime.strptime(
340
            d, AnalysisClientBase.DATETIME_MSEC_FMT)
341
    except ValueError: pass
342
343
    try:
344
        return datetime.datetime.strptime(d, AnalysisClientBase.DATETIME_FMT)
345
    except ValueError: pass
346
347
    try:
348
        return datetime.datetime.strptime(d, AnalysisClientBase.DATE_FMT)
349
    except ValueError:
350
        raise ValueError("Date '%s' does not match format '%s'" % (
351
                         d, "%Y-%m-%d[ %H:%M:%S[.%f]]'"))
352
353
354
class TaskCompletion(object):
355
    """
356
    Helper class to get score for all completed tasks
357
358
    :param analysis_client: analysis_apiclient.AnalysisClientBase
359
360
    Sample usage:
361
362
    tc = TaskCompletion(my_analysis_client)
363
    for completed_task in tc.get_completed(start,end):
364
        print completed_task.task_uuid, completed_task.score
365
366
    """
367
    def __init__(self, analysis_client):
368
        self.__analysis_client = analysis_client
369
370
    def get_completed(self, after, before):
371
        """
372
        Return scores of tasks completed in the specified time range.
373
374
        This takes care of using the analysis API's pagination
375
        to make sure it gets all tasks.
376
377
        :param after: datetime.datetime
378
        :param before: datetime.datetime
379
380
        :yield: sequence of `CompletedTask`
381
382
        :raise: InvalidAnalysisAPIResponse if response
383
            does not have the format we expect
384
        """
385
        try:
386
            while True:
387
                result = self.__analysis_client.get_completed(
388
                    after=after,
389
                    before=before,
390
                    include_score=True)
391
392
                data = result["data"]
393
                tasks = data["tasks"]
394
                if not tasks:
395
                    break
396
397
                for task_uuid, score  in tasks.iteritems():
398
                    yield CompletedTask(task_uuid=task_uuid,
399
                                        score=score)
400
401
                more = int(data["more_results_available"])
402
                if not more:
403
                    break
404
405
                last_ts = parse_datetime(data["before"])
406
                if last_ts >= before:
407
                    break
408
409
                after = last_ts
410
411
        except (KeyError, ValueError, TypeError, AttributeError):
412
            # attributeError needed in case iteritems is missing (not a dict)
413
            # let's give it the trace of the original exception, so we know
414
            # what the specific problem is!
415
            trace = sys.exc_info()[2]
416
            raise InvalidAnalysisAPIResponse("Unable to parse response to get_completed()"), None, trace
417
418
419
class AnalysisClientBase(object):
420
    """
421
    A client for the Lastline analysis API.
422
423
    This is an abstract base class: concrete
424
    subclasses just need to implement the _api_request
425
    method to actually send the API request to the server.
426
427
    :param base_url: URL where the lastline analysis API is located. (required)
428
    :param logger: if provided, should be a python logging.Logger object
429
        or object with similar interface.
430
    """
431
    SUB_APIS = ('analysis', 'management', 'research')
432
433
    DATETIME_FMT = '%Y-%m-%d %H:%M:%S'
434
    DATETIME_MSEC_FMT = DATETIME_FMT + '.%f'
435
    DATE_FMT = '%Y-%m-%d'
436
437
    FORMATS = ["json", "xml", "pdf", "rtf"]
438
439
    REQUEST_PERFDATA = False
440
441
    ERRORS = {
442
        ANALYSIS_API_FILE_NOT_AVAILABLE: FileNotAvailableError,
443
        ANALYSIS_API_INVALID_CREDENTIALS: InvalidCredentialsError,
444
        ANALYSIS_API_INVALID_UUID: InvalidUUIDError,
445
        ANALYSIS_API_NO_RESULT_FOUND: NoResultFoundError,
446
        ANALYSIS_API_TEMPORARILY_UNAVAILABLE: TemporarilyUnavailableError,
447
        ANALYSIS_API_PERMISSION_DENIED: PermissionDeniedError,
448
        ANALYSIS_API_FILE_TOO_LARGE: FileTooLargeError,
449
        ANALYSIS_API_INVALID_FILE_TYPE: InvalidFileTypeError,
450
        ANALYSIS_API_INVALID_DOMAIN: InvalidMetadataError,
451
        ANALYSIS_API_INVALID_D_METADATA: InvalidMetadataError,
452
        ANALYSIS_API_INVALID_ARTIFACT_UUID: InvalidArtifactError,
453
        ANALYSIS_API_SUBMISSION_LIMIT_EXCEEDED: SubmissionLimitExceededError,
454
        ANALYSIS_API_INVALID_HASH_ALGORITHM: InvalidHashAlgorithmError,
455
        ANALYSIS_API_INVALID_URL: InvalidURLError,
456
        ANALYSIS_API_INVALID_REPORT_VERSION: InvalidReportVersionError,
457
        ANALYSIS_API_FILE_EXTRACTION_FAILED: FileExtractionFailedError,
458
      }
459
460
    def __init__(self, base_url, logger=None, config=None):
461
        self.__logger = logger
462
        self.__base_url = base_url
463
        self.__config = config
464
465
    def _logger(self):
466
        return self.__logger
467
468
    def __build_url(self, sub_api, parts, requested_format="json"):
469
        if sub_api not in AnalysisClientBase.SUB_APIS:
470
            raise InvalidSubApiType(sub_api)
471
        if requested_format not in AnalysisClientBase.FORMATS:
472
            raise InvalidFormat(requested_format)
473
        num_parts = 2 + len(parts)
474
        pattern = "/".join(["%s"] * num_parts) + ".%s"
475
        params = [self.__base_url, sub_api] + parts + [requested_format]
476
        return pattern % tuple(params)
477
478
    def __build_file_download_url(self, sub_api, parts):
479
        """
480
        Generate a URL to a direct file download
481
        """
482
        if sub_api not in AnalysisClientBase.SUB_APIS:
483
            raise InvalidSubApiType(sub_api)
484
        num_parts = 2 + len(parts)
485
        pattern = "/".join(["%s"] * num_parts)
486
        params = [self.__base_url, sub_api] + parts
487
        return pattern % tuple(params)
488
489
    def _check_file_like(self, f, param_name):
490
        if not hasattr(f, 'read'):
491
            raise AttributeError("The %s parameter is not a file-like " \
492
                                 "object" % param_name)
493
494
    def submit_exe_hash(self,
495
                        md5=None,
496
                        sha1=None,
497
                        download_ip=None,
498
                        download_port=None,
499
                        download_url=None,
500
                        download_host=None,
501
                        download_path=None,
502
                        download_agent=None,
503
                        download_referer=None,
504
                        download_request=None,
505
                        full_report_score=None,
506
                        bypass_cache=None,
507
                        raw=False,
508
                        verify=True):
509
        """
510
        Submit a file by hash.
511
512
        Deprecated version of submit_file_hash() - see below
513
        """
514
        return self.submit_file_hash(md5, sha1,
515
                        download_ip=download_ip,
516
                        download_port=download_port,
517
                        download_url=download_url,
518
                        download_host=download_host,
519
                        download_path=download_path,
520
                        download_agent=download_agent,
521
                        download_referer=download_referer,
522
                        download_request=download_request,
523
                        full_report_score=full_report_score,
524
                        bypass_cache=bypass_cache,
525
                        raw=raw,
526
                        verify=verify)
527
528
    def submit_file_hash(self,
529
                        md5=None,
530
                        sha1=None,
531
                        download_ip=None,
532
                        download_port=None,
533
                        download_url=None,
534
                        download_host=None,
535
                        download_path=None,
536
                        download_agent=None,
537
                        download_referer=None,
538
                        download_request=None,
539
                        full_report_score=None,
540
                        bypass_cache=None,
541
                        backend=None,
542
                        require_file_analysis=True,
543
                        mime_type=None,
544
                        analysis_timeout=None,
545
                        analysis_env=None,
546
                        allow_network_traffic=None,
547
                        filename=None,
548
                        keep_file_dumps=None,
549
                        keep_memory_dumps=None,
550
                        keep_behavior_log=None,
551
                        push_to_portal_account=None,
552
                        raw=False,
553
                        verify=True,
554
                        server_ip=None,
555
                        server_port=None,
556
                        server_host=None,
557
                        client_ip=None,
558
                        client_port=None,
559
                        is_download=True,
560
                        protocol="http",
561
                        apk_package_name=None,
562
                        report_version=None):
563
        """
564
        Submit a file by hash.
565
566
        Either an md5 or a sha1 parameter must be provided.
567
        If both are provided, they should be consistent.
568
569
        For return values and error codes please
570
        see :py:meth:`malscape.api.views.analysis.submit_file`.
571
572
        If there is an error and `raw` is not set,
573
        a :py:class:`AnalysisAPIError` exception will be raised.
574
575
        :param md5: md5 hash of file.
576
        :param sha1: sha1 hash of file.
577
        :param download_ip: DEPRECATED! Use server_ip instead.
578
        :param download_port: DEPRECATED! Use server_port instead.
579
        :param download_url: DEPRECATED! replaced by the download_host
580
            and download_path parameters
581
        :param download_host: DEPRECATED! Use server_host instead.
582
        :param download_path: host path from which the submitted file
583
            was originally downloaded, as a string of bytes (not unicode)
584
        :param download_agent: HTTP user-agent header that was used
585
            when the submitted file was originally downloaded,
586
            as a string of bytes (not unicode)
587
        :param download_referer: HTTP referer header that was used
588
            when the submitted file was originally downloaded,
589
            as a string of bytes (not unicode)
590
        :param download_request: full HTTP request with
591
            which the submitted file was originally downloaded,
592
            as a string of bytes (not unicode)
593
        :param full_report_score: if set, this value (between -1 and 101)
594
            determines starting at which scores a full report is returned.
595
            -1 and 101 indicate "never return full report";
596
            0 indicates "return full report at all times"
597
        :param bypass_cache: if True, the API will not serve a cached
598
            result. NOTE: This requires special privileges.
599
        :param require_file_analysis: if True, the submission requires an
600
            analysis run to be started. If False, the API will attempt to
601
            base a decision solely on static information such as
602
            download source reputation and hash lookups. Requires special
603
            permissions
604
        :param mime_type: the mime-type of the file; This value should be
605
            set when require_file_analysis is True to enforce getting the
606
            most information available
607
        :param analysis_timeout: timeout in seconds after which to terminate
608
            analysis. The analysis engine might decide to extend this timeout
609
            if necessary. If all analysis subjects terminate before this timeout
610
            analysis might be shorter
611
        :param analysis_env: environment in which to run analysis. This includes
612
            the operating system as well as version of tools such as Microsoft
613
            Office. Example usage:
614
            - windows7:office2003, or
615
            - windowsxp
616
            By default, analysis will run on all available operating systems
617
            using the most applicable tools.
618
        :param allow_network_traffic: if False, all network connections will be
619
            redirected to a honeypot. Requires special permissions.
620
        :param filename: filename to use during analysis. If none is passed,
621
            the analysis engine will pick an appropriate name automatically.
622
            An easy way to pass this value is to use 'file_stream.name' for most
623
            file-like objects
624
        :param keep_file_dumps: if True, all files generated during
625
            analysis will be kept for post-processing. NOTE: This can generate
626
            large volumes of data and is not recommended. Requires special
627
            permissions
628
        :param keep_memory_dumps: if True, all buffers allocated during
629
            analysis will be kept for post-processing. NOTE: This can generate
630
            *very* large volumes of data and is not recommended. Requires
631
            special permissions
632
        :param keep_behavior_log: if True, the raw behavior log extracted during
633
            analysis will be kept for post-processing. NOTE: This can generate
634
            *very very* large volumes of data and is not recommended. Requires
635
            special permissions
636
        :param push_to_portal_account: if set, a successful submission will be
637
            pushed to the web-portal using the specified account
638
        :param backend: DEPRECATED! Don't use
639
        :param verify: if False, disable SSL-certificate verification
640
        :param raw: if True, return the raw json results of the API query
641
        :param server_ip: ASCII dotted-quad representation of the IP address of
642
            the server-side endpoint.
643
        :param server_port: integer representation of the port number
644
            of the server-side endpoint of the flow tuple.
645
        :param server_host: hostname of the server-side endpoint of
646
            the connection, as a string of bytes (not unicode).
647
        :param client_ip: ASCII dotted-quad representation of the IP address of
648
            the client-side endpoint.
649
        :param client_port: integer representation of the port number
650
            of the client-side endpoint of the flow tuple.
651
        :param is_download: Boolean; True if the transfer happened in the
652
            server -> client direction, False otherwise (client -> server).
653
        :param protocol: app-layer protocol in which the file got
654
            transferred. Short ASCII string.
655
        :param apk_package_name: package name for APK files. Don't specify
656
            manually.
657
        :param report_version: Version name of the Report that will be returned
658
                               (optional);
659
        """
660
        if self.__logger and backend:
661
            self.__logger.warning("Ignoring deprecated parameter 'backend'")
662
663
        url = self.__build_url("analysis", ["submit", "file"])
664
        # These options require special permissions, so we should not set them
665
        # if not specified
666
        if allow_network_traffic is not None:
667
            allow_network_traffic = allow_network_traffic and 1 or 0
668
        if keep_file_dumps is not None:
669
            keep_file_dumps = keep_file_dumps and 1 or 0
670
        if keep_memory_dumps is not None:
671
            keep_memory_dumps = keep_memory_dumps and 1 or 0
672
        if keep_behavior_log is not None:
673
            keep_behavior_log = keep_behavior_log and 1 or 0
674
        params = purge_none({
675
            "md5": md5,
676
            "sha1": sha1,
677
            "full_report_score": full_report_score,
678
            "bypass_cache": bypass_cache and 1 or None,
679
            "require_file_analysis": require_file_analysis and 1 or 0,
680
            "mime_type": mime_type,
681
            "download_ip": download_ip,
682
            "download_port": download_port,
683
            # analysis-specific options:
684
            "analysis_timeout": analysis_timeout or None,
685
            "analysis_env": analysis_env,
686
            "allow_network_traffic": allow_network_traffic,
687
            "filename": filename,
688
            "keep_file_dumps": keep_file_dumps,
689
            "keep_memory_dumps": keep_memory_dumps,
690
            "keep_behavior_log": keep_behavior_log,
691
            "push_to_portal_account": push_to_portal_account or None,
692
            "server_ip": server_ip,
693
            "server_port": server_port,
694
            "server_host": server_host,
695
            "client_ip": client_ip,
696
            "client_port": client_port,
697
            "is_download": is_download,
698
            "protocol": protocol,
699
            "apk_package_name": apk_package_name,
700
            "report_version": report_version,
701
          })
702
        # using and-or-trick to convert to a StringIO if it is not None
703
        # this just wraps it into a file-like object
704
        files = purge_none({
705
            "download_url": download_url is not None and \
706
                               StringIO.StringIO(download_url) or None,
707
            "download_host": download_host is not None and \
708
                               StringIO.StringIO(download_host) or None,
709
            "download_path": download_path is not None and \
710
                               StringIO.StringIO(download_path) or None,
711
            "download_agent": download_agent is not None and \
712
                               StringIO.StringIO(download_agent) or None,
713
            "download_referer": download_referer is not None and \
714
                               StringIO.StringIO(download_referer) or None,
715
            "download_request": download_request is not None and \
716
                               StringIO.StringIO(download_request) or None,
717
            "server_host": server_host is not None and \
718
                               StringIO.StringIO(server_host) or None,
719
          })
720
        return self._api_request(url, params, files=files, post=True,
721
                                 raw=raw, verify=verify)
722
723
    def submit_exe_file(self,
724
                        file_stream,
725
                        download_ip=None,
726
                        download_port=None,
727
                        download_url=None,
728
                        download_host=None,
729
                        download_path=None,
730
                        download_agent=None,
731
                        download_referer=None,
732
                        download_request=None,
733
                        full_report_score=None,
734
                        bypass_cache=None,
735
                        delete_after_analysis=False,
736
                        raw=False,
737
                        verify=True):
738
        """
739
        Submit a file by uploading it.
740
741
        Deprecated version of submit_file() - see below
742
        """
743
        return self.submit_file(file_stream,
744
                        download_ip=download_ip,
745
                        download_port=download_port,
746
                        download_url=download_url,
747
                        download_host=download_host,
748
                        download_path=download_path,
749
                        download_agent=download_agent,
750
                        download_referer=download_referer,
751
                        download_request=download_request,
752
                        full_report_score=full_report_score,
753
                        bypass_cache=bypass_cache,
754
                        delete_after_analysis=delete_after_analysis,
755
                        raw=raw,
756
                        verify=verify)
757
758
    def submit_file(self, file_stream,
759
                    download_ip=None,
760
                    download_port=None,
761
                    download_url=None,
762
                    download_host=None,
763
                    download_path=None,
764
                    download_agent=None,
765
                    download_referer=None,
766
                    download_request=None,
767
                    full_report_score=None,
768
                    bypass_cache=None,
769
                    delete_after_analysis=None,
770
                    backend=None,
771
                    analysis_timeout=None,
772
                    analysis_env=None,
773
                    allow_network_traffic=None,
774
                    filename=None,
775
                    keep_file_dumps=None,
776
                    keep_memory_dumps=None,
777
                    keep_behavior_log=None,
778
                    push_to_portal_account=None,
779
                    raw=False,
780
                    verify=True,
781
                    server_ip=None,
782
                    server_port=None,
783
                    server_host=None,
784
                    client_ip=None,
785
                    client_port=None,
786
                    is_download=True,
787
                    protocol="http",
788
                    apk_package_name=None,
789
                    password=None,
790
                    report_version=None):
791
        """
792
        Submit a file by uploading it.
793
794
        For return values and error codes please
795
        see :py:meth:`malscape.api.views.analysis.submit_file`.
796
797
        If there is an error and `raw` is not set,
798
        a :py:class:`AnalysisAPIError` exception will be raised.
799
800
        :param file_stream: file-like object containing
801
            the file to upload.
802
        :param download_ip: DEPRECATED! Use server_ip instead.
803
        :param download_port: DEPRECATED! Use server_port instead.
804
        :param download_url: DEPRECATED! replaced by the download_host
805
            and download_path parameters
806
        :param download_host: DEPRECATED! Use server_host instead.
807
        :param download_path: host path from which the submitted file
808
            was originally downloaded, as a string of bytes (not unicode)
809
        :param download_agent: HTTP user-agent header that was used
810
            when the submitted file was originally downloaded,
811
            as a string of bytes (not unicode)
812
        :param download_referer: HTTP referer header that was used
813
            when the submitted file was originally downloaded,
814
            as a string of bytes (not unicode)
815
        :param download_request: full HTTP request with
816
            which the submitted file was originally downloaded,
817
            as a string of bytes (not unicode)
818
        :param full_report_score: if set, this value (between -1 and 101)
819
            determines starting at which scores a full report is returned.
820
            -1 and 101 indicate "never return full report";
821
            0 indicates "return full report at all times"
822
        :param bypass_cache: if True, the API will not serve a cached
823
            result. NOTE: This requires special privileges.
824
        :param delete_after_analysis: if True, the backend will delete the
825
            file after analysis is done (and noone previously submitted
826
            this file with this flag set)
827
        :param analysis_timeout: timeout in seconds after which to terminate
828
            analysis. The analysis engine might decide to extend this timeout
829
            if necessary. If all analysis subjects terminate before this timeout
830
            analysis might be shorter
831
        :param analysis_env: environment in which to run analysis. This includes
832
            the operating system as well as version of tools such as Microsoft
833
            Office. Example usage:
834
            - windows7:office2003, or
835
            - windowsxp
836
            By default, analysis will run on all available operating systems
837
            using the most applicable tools.
838
        :param allow_network_traffic: if False, all network connections will be
839
            redirected to a honeypot. Requires special permissions.
840
        :param filename: filename to use during analysis. If none is passed,
841
            the analysis engine will pick an appropriate name automatically.
842
            An easy way to pass this value is to use 'file_stream.name' for most
843
            file-like objects
844
        :param keep_file_dumps: if True, all files generated during
845
            analysis will be kept for post-processing. NOTE: This can generate
846
            large volumes of data and is not recommended. Requires special
847
            permissions
848
        :param keep_memory_dumps: if True, all buffers allocated during
849
            analysis will be kept for post-processing. NOTE: This can generate
850
            large volumes of data and is not recommended. Requires special
851
            permissions
852
        :param keep_behavior_log: if True, the raw behavior log extracted during
853
            analysis will be kept for post-processing. NOTE: This can generate
854
            *very very* large volumes of data and is not recommended. Requires
855
            special permissions
856
        :param push_to_portal_account: if set, a successful submission will be
857
            pushed to the web-portal using the specified username
858
        :param backend: DEPRECATED! Don't use
859
        :param verify: if False, disable SSL-certificate verification
860
        :param raw: if True, return the raw JSON results of the API query
861
        :param server_ip: ASCII dotted-quad representation of the IP address of
862
            the server-side endpoint.
863
        :param server_port: integer representation of the port number
864
            of the server-side endpoint of the flow tuple.
865
        :param server_host: hostname of the server-side endpoint of
866
            the connection, as a string of bytes (not unicode).
867
        :param client_ip: ASCII dotted-quad representation of the IP address of
868
            the client-side endpoint.
869
        :param client_port: integer representation of the port number
870
            of the client-side endpoint of the flow tuple.
871
        :param is_download: Boolean; True if the transfer happened in the
872
            server -> client direction, False otherwise (client -> server).
873
        :param protocol: app-layer protocol in which the file got
874
            transferred. Short ASCII string.
875
        :param report_version: Version name of the Report that will be returned
876
                               (optional);
877
        :param apk_package_name: package name for APK files. Don't specify
878
            manually.
879
        :param password: password used to unpack encrypted archives
880
        """
881
        if self.__logger and backend:
882
            self.__logger.warning("Ignoring deprecated parameter 'backend'")
883
884
        self._check_file_like(file_stream, "file_stream")
885
        url = self.__build_url("analysis", ["submit", "file"])
886
        # These options require special permissions, so we should not set them
887
        # if not specified
888
        if allow_network_traffic is not None:
889
            allow_network_traffic = allow_network_traffic and 1 or 0
890
        if keep_file_dumps is not None:
891
            keep_file_dumps = keep_file_dumps and 1 or 0
892
        if keep_memory_dumps is not None:
893
            keep_memory_dumps = keep_memory_dumps and 1 or 0
894
        if keep_behavior_log is not None:
895
            keep_behavior_log = keep_behavior_log and 1 or 0
896
        params = purge_none({
897
            "bypass_cache": bypass_cache and 1 or None,
898
            "full_report_score": full_report_score,
899
            "delete_after_analysis": delete_after_analysis and 1 or 0,
900
            "download_ip": download_ip,
901
            "download_port": download_port,
902
            # analysis-specific options:
903
            "analysis_timeout": analysis_timeout or None,
904
            "analysis_env": analysis_env,
905
            "allow_network_traffic": allow_network_traffic,
906
            "filename": filename,
907
            "keep_file_dumps": keep_file_dumps,
908
            "keep_memory_dumps": keep_memory_dumps,
909
            "keep_behavior_log": keep_behavior_log,
910
            "push_to_portal_account": push_to_portal_account or None,
911
            "server_ip": server_ip,
912
            "server_port": server_port,
913
            "server_host": server_host,
914
            "client_ip": client_ip,
915
            "client_port": client_port,
916
            "is_download": is_download,
917
            "protocol": protocol,
918
            "apk_package_name": apk_package_name,
919
            "password": password,
920
            "report_version": report_version,
921
          })
922
923
        # If an explicit filename was provided, we can pass it down to
924
        # python-requests to use it in the multipart/form-data.
925
        # This avoids having python-requests trying to guess the filename
926
        # based on stream attributes.
927
        named_stream = (filename, file_stream) if filename else file_stream
928
929
        # using and-or-trick to convert to a StringIO if it is not None
930
        # this just wraps it into a file-like object
931
        files = purge_none({
932
            "file": named_stream,
933
            "download_url": download_url is not None and \
934
                                  StringIO.StringIO(download_url) or None,
935
            "download_host": download_host is not None and \
936
                                  StringIO.StringIO(download_host) or None,
937
            "download_path": download_path is not None and \
938
                                  StringIO.StringIO(download_path) or None,
939
            "download_agent": download_agent is not None and \
940
                                  StringIO.StringIO(download_agent) or None,
941
            "download_referer": download_referer is not None and \
942
                                  StringIO.StringIO(download_referer) or None,
943
            "download_request": download_request is not None and \
944
                                  StringIO.StringIO(download_request) or None,
945
            "server_host": server_host is not None and \
946
                                  StringIO.StringIO(server_host) or None,
947
          })
948
        return self._api_request(url, params, files=files, post=True,
949
                                 raw=raw, verify=verify)
950
951
952
    def submit_file_metadata(self, md5, sha1,
953
                                   download_ip,
954
                                   download_port,
955
                                   download_host=None,
956
                                   download_path=None,
957
                                   download_agent=None,
958
                                   download_referer=None,
959
                                   download_request=None,
960
                                   raw=False,
961
                                   verify=True):
962
        """
963
        Submit metadata regarding a file download.
964
965
        Both the md5 and the sha1 parameter must be provided.
966
967
        If there is an error and `raw` is not set,
968
        a :py:class:`AnalysisAPIError` exception will be raised.
969
970
        :param md5: md5 hash of the downloaded file.
971
        :param sha1: sha1 hash of the downloaded file.
972
        :param download_ip: ASCII dotted-quad representation of the IP address
973
            from which the file has been downloaded
974
        :param download_port: integer representation of the port number
975
            from which the file has been downloaded
976
        :param download_host: host from which the submitted file
977
            was originally downloaded, as a string of bytes (not unicode)
978
        :param download_path: host path from which the submitted file
979
            was originally downloaded, as a string of bytes (not unicode)
980
        :param download_agent: HTTP user-agent header that was used
981
            when the submitted file was originally downloaded,
982
            as a string of bytes (not unicode)
983
        :param download_referer: HTTP referer header that was used
984
            when the submitted file was originally downloaded,
985
            as a string of bytes (not unicode)
986
        :param download_request: full HTTP request with
987
            which the submitted file was originally downloaded,
988
            as a string of bytes (not unicode)
989
        :param verify: if False, disable SSL-certificate verification
990
        :param raw: if True, return the raw json results of the API query
991
        """
992
        url = self.__build_url("analysis", ["submit", "download"])
993
        params = {
994
            "md5": md5,
995
            "sha1": sha1,
996
            "download_ip": download_ip,
997
            "download_port": download_port
998
          }
999
        #using and-or-trick to convert to a StringIO if it is not None
1000
        #this just wraps it into a file-like object
1001
        files = {
1002
            "download_host": download_host is not None and \
1003
                                   StringIO.StringIO(download_host) or None,
1004
            "download_path": download_path is not None and \
1005
                                   StringIO.StringIO(download_path) or None,
1006
            "download_agent": download_agent is not None and \
1007
                                   StringIO.StringIO(download_agent) or None,
1008
            "download_referer": download_referer is not None and \
1009
                                   StringIO.StringIO(download_referer) or None,
1010
            "download_request": download_request is not None and \
1011
                                   StringIO.StringIO(download_request) or None
1012
1013
          }
1014
        purge_none(files)
1015
        purge_none(params)
1016
        return self._api_request(url, params, files=files, post=True,
1017
                                 raw=raw, verify=verify)
1018
1019
1020
    def submit_url(self,
1021
                   url,
1022
                   referer=None,
1023
                   full_report_score=None,
1024
                   bypass_cache=None,
1025
                   backend=None,
1026
                   analysis_timeout=None,
1027
                   push_to_portal_account=None,
1028
                   raw=False,
1029
                   verify=True,
1030
                   user_agent=None,
1031
                   report_version=None):
1032
        """
1033
        Submit a url.
1034
1035
        For return values and error codes please
1036
        see :py:meth:`malscape.api.views.analysis.submit_url`.
1037
1038
        If there is an error and `raw` is not set,
1039
        a :py:class:`AnalysisAPIError` exception will be raised.
1040
1041
        :param url: url to analyze
1042
        :param referer: referer header to use for analysis
1043
        :param full_report_score: if set, this value (between -1 and 101)
1044
            determines starting at which scores a full report is returned.
1045
            -1 and 101 indicate "never return full report";
1046
            0 indicates "return full report at all times"
1047
        :param bypass_cache: if True, the API will not serve a cached
1048
            result. NOTE: This requires special privileges.
1049
        :param analysis_timeout: timeout in seconds after which to terminate
1050
            analysis. The analysis engine might decide to extend this timeout
1051
            if necessary. If all analysis subjects terminate before this timeout
1052
            analysis might be shorter
1053
        :param push_to_portal_account: if set, a successful submission will be
1054
            pushed to the web-portal using the specified account
1055
        :param backend: DEPRECATED! Don't use
1056
        :param verify: if False, disable SSL-certificate verification
1057
        :param raw: if True, return the raw JSON results of the API query
1058
        :param report_version: Version name of the Report that will be returned
1059
                               (optional);
1060
        :param user_agent: user agent header to use for analysis
1061
        """
1062
        if self.__logger and backend:
1063
            self.__logger.warning("Ignoring deprecated parameter 'backend'")
1064
1065
        api_url = self.__build_url("analysis", ["submit", "url"])
1066
        params = purge_none({
1067
            "url":url,
1068
            "referer":referer,
1069
            "full_report_score":full_report_score,
1070
            "bypass_cache":bypass_cache and 1 or None,
1071
            "analysis_timeout": analysis_timeout or None,
1072
            "push_to_portal_account": push_to_portal_account or None,
1073
            "user_agent": user_agent or None,
1074
            "report_version" : report_version,
1075
          })
1076
        return self._api_request(api_url, params, post=True,
1077
                                 raw=raw, verify=verify)
1078
1079
    def get_result(self,
1080
                   uuid,
1081
                   report_uuid=None,
1082
                   full_report_score=None,
1083
                   include_scoring_components=None,
1084
                   raw=False,
1085
                   requested_format="json",
1086
                   verify=True,
1087
                   report_version=None):
1088
        """
1089
        Get results for a previously submitted
1090
        analysis task.
1091
1092
        For return values and error codes please
1093
        see :py:meth:`malscape.api.views.analysis.get_results`.
1094
1095
        If there is an error and `raw` is not set,
1096
        a :py:class:`AnalysisAPIError` exception will be raised.
1097
1098
        :param uuid: the unique identifier of the submitted task,
1099
            as returned in the task_uuid field of submit methods.
1100
        :param report_uuid: if set, include this report in the result.
1101
        :param full_report_score: if set, this value (between -1 and 101)
1102
            determines starting at which scores a full report is returned.
1103
            -1 and 101 indicate "never return full report";
1104
            0 indicates "return full report at all times"
1105
        :param include_scoring_components: if True, the result will contain
1106
            details of all components contributing to the overall score.
1107
            Requires special permissions
1108
        :param raw: if True, return the raw JSON/XML results of the API query.
1109
        :param requested_format: JSON, XML, PDF, or RTF.
1110
            If format is not JSON, this implies `raw`.
1111
        :param report_version: Version of the report to be returned
1112
                               (optional)
1113
        """
1114
        # better: use 'get_results()' but that would break
1115
        # backwards-compatibility
1116
        url = self.__build_url('analysis', ['get'],
1117
                               requested_format=requested_format)
1118
        params = purge_none({
1119
            'uuid': uuid,
1120
            'report_uuid': report_uuid,
1121
            'full_report_score': full_report_score,
1122
            'include_scoring_components': include_scoring_components and 1 or 0,
1123
            'report_version': report_version
1124
          })
1125
        if requested_format.lower() != 'json':
1126
            raw = True
1127
        return self._api_request(url,
1128
                                 params,
1129
                                 raw=raw,
1130
                                 requested_format=requested_format,
1131
                                 post=True,
1132
                                 verify=verify)
1133
1134
    def get_result_summary(self, uuid, raw=False,
1135
                           requested_format="json",
1136
                           score_only=False,
1137
                           verify=True):
1138
        """
1139
        Get result summary for a previously submitted analysis task.
1140
1141
        For return values and error codes please
1142
        see :py:meth:`malscape.api.views.analysis.get_result`.
1143
1144
        If there is an error and `raw` is not set,
1145
        a :py:class:`AnalysisAPIError` exception will be raised.
1146
1147
        :param uuid: the unique identifier of the submitted task,
1148
            as returned in the task_uuid field of submit methods.
1149
        :param raw: if True, return the raw JSON/XML results of the API query.
1150
        :param requested_format: JSON or XML. If format is not JSON, this
1151
            implies `raw`.
1152
        :param score_only: if True, return even less data (only score and
1153
            threat/threat-class classification).
1154
        """
1155
        url = self.__build_url("analysis", ["get_result"],
1156
                               requested_format=requested_format)
1157
        params = {
1158
            'uuid': uuid,
1159
            'score_only': score_only and 1 or 0,
1160
          }
1161
        if requested_format.lower() != "json":
1162
            raw = True
1163
        return self._api_request(url,
1164
                                 params,
1165
                                 raw=raw,
1166
                                 requested_format=requested_format,
1167
                                 post=True,
1168
                                 verify=verify)
1169
1170
    def get_result_artifact(self, uuid, report_uuid, artifact_name,
1171
                            raw=False, verify=True):
1172
        """
1173
        Get artifact generated by an analysis result for a previously
1174
        submitted analysis task.
1175
1176
        :param uuid: the unique identifier of the submitted task,
1177
            as returned in the task_uuid field of submit methods.
1178
        :param report_uuid: the unique report identifier returned as part of
1179
            the dictionary returned by get_result()
1180
        :param artifact_name: the name of the artifact as mentioned in the
1181
            given report in the dictionary returned by get_result()
1182
        :param raw: if True, return the raw JSON/XML results of the API query.
1183
        """
1184
        url = self.__build_file_download_url("analysis",
1185
                                             ["get_result_artifact"])
1186
        params = {
1187
            'uuid': uuid,
1188
            'artifact_uuid': "%s:%s" % (report_uuid, artifact_name)
1189
          }
1190
1191
        # NOTE: This API request is completely different because it
1192
        # returns real HTTP status-codes (and errors) directly
1193
        try:
1194
            result = self._api_request(url, params, requested_format='raw',
1195
                                       raw=raw, post=True, verify=verify)
1196
            if not result:
1197
                raise InvalidArtifactError()
1198
1199
        except CommunicationError, exc:
1200
            internal_error = str(exc.internal_error())
1201
            if internal_error == '410':
1202
                raise InvalidArtifactError("The artifact is no longer " \
1203
                                           "available")
1204
            if internal_error == '404':
1205
                raise InvalidArtifactError("The artifact could not be found")
1206
1207
            if internal_error == '412':
1208
                raise InvalidUUIDError()
1209
1210
            if internal_error == '412':
1211
                raise InvalidUUIDError()
1212
1213
            if internal_error == '401':
1214
                raise PermissionDeniedError()
1215
1216
            # we have nothing more specific to say -- raise the
1217
            # original CommunicationError
1218
            raise
1219
1220
        return StringIO.StringIO(result)
1221
1222
    def query_task_artifact(self, uuid, artifact_name, raw=False, verify=True):
1223
        """
1224
        Query if a specific task artifact is available for download.
1225
1226
        :param uuid: the unique identifier of the submitted task,
1227
            as returned in the task_uuid field of submit methods.
1228
        :param artifact_name: the name of the artifact
1229
        :param raw: if True, return the raw JSON/XML results of the API query.
1230
        """
1231
        url = self.__build_url("analysis", ["query_task_artifact"])
1232
        params = purge_none({
1233
            'uuid': uuid,
1234
            'artifact_name': artifact_name,
1235
          })
1236
        return self._api_request(url, params, raw=raw, verify=verify)
1237
1238
    def completed(self, after, before=None, raw=False, verify=True):
1239
        """
1240
        Deprecated. Use 'get_completed()'
1241
        """
1242
        return self.get_completed(after, before=before,
1243
                                  verify=verify, raw=raw)
1244
1245
    def get_completed(self, after, before=None, raw=False, verify=True,
1246
                      include_score=False):
1247
        """
1248
        Get the list of uuids of tasks that were completed
1249
        within a given time frame.
1250
1251
        The main use-case for this method is to periodically
1252
        request a list of uuids completed since the last
1253
        time this method was invoked, and then fetch
1254
        each result with `get_results()`.
1255
1256
        Date parameters to this method can be:
1257
         - date string: %Y-%m-%d'
1258
         - datetime string: '%Y-%m-%d %H:%M:%S'
1259
         - datetime.datetime object
1260
1261
        All times are in UTC.
1262
1263
        For return values and error codes please
1264
        see :py:meth:`malscape.api.views.analysis.get_completed`.
1265
1266
        If there is an error and `raw` is not set,
1267
        a :py:class:`AnalysisAPIError` exception will be raised.
1268
1269
        :param after: Request tasks completed after this time.
1270
        :param before: Request tasks completed before this time.
1271
        :param include_score: If True, the response contains scores together
1272
            with the task-UUIDs that have completed
1273
        :param raw: if True, return the raw JSON results of the API query.
1274
        """
1275
        # better: use 'get_completed()' but that would break
1276
        # backwards-compatibility
1277
        url = self.__build_url("analysis", ["completed"])
1278
        if hasattr(before, "strftime"):
1279
            before = before.strftime(AnalysisClientBase.DATETIME_FMT)
1280
        if hasattr(after, "strftime"):
1281
            after = after.strftime(AnalysisClientBase.DATETIME_FMT)
1282
        params = purge_none({
1283
            'before': before,
1284
            'after': after,
1285
            'include_score': include_score and 1 or 0,
1286
          })
1287
        return self._api_request(url, params, raw=raw, post=True, verify=verify)
1288
1289
    def get_progress(self, uuid, raw=False):
1290
        """
1291
        Get a progress estimate for a previously submitted analysis task.
1292
1293
        For return values and error codes please
1294
        see :py:meth:`malscape.api.views.analysis.get_results`.
1295
1296
        If there is an error and `raw` is not set,
1297
        a :py:class:`AnalysisAPIError` exception will be raised.
1298
1299
        :param uuid: the unique identifier of the submitted task,
1300
            as returned in the task_uuid field of submit methods.
1301
        :param raw: if True, return the raw JSON/XML results of the API query.
1302
        :param requested_format: JSON or XML. If format is not JSON, this implies `raw`.
1303
        """
1304
        url = self.__build_url('analysis', ['get_progress'])
1305
        params = { 'uuid': uuid }
1306
        return self._api_request(url, params, raw=raw, post=True)
1307
1308
    def query_file_hash(self, hash_value=None, algorithm=None, block_size=None,
1309
                        md5=None, sha1=None, mmh3=None, raw=False):
1310
        """
1311
        Search for existing analysis results with the given file-hash.
1312
1313
        :param hash_value: The (partial) file-hash.
1314
        :param algorithm: One of MD5/SHA1/MMH3
1315
        :param block_size: Size of the block (at file start) used for generating
1316
            the hash-value. By default (or if 0), the entire file is assumed.
1317
        :param md5: Helper to quickly set `hash_value` and `algorithm`
1318
        :param sha1: Helper to quickly set `hash_value` and `algorithm`
1319
        :param mmh3: Helper to quickly set `hash_value` and `algorithm`
1320
        :param raw: if True, return the raw JSON/XML results of the API query.
1321
        :param requested_format: JSON or XML. If format is not JSON, this
1322
            implies `raw`.
1323
        """
1324
        if md5 or sha1 or mmh3:
1325
            if hash_value or algorithm:
1326
                raise TypeError("Conflicting values passed for hash/algorithm")
1327
            if md5 and not sha1 and not mmh3:
1328
                hash_value = md5
1329
                algorithm = 'md5'
1330
            elif sha1 and not md5 and not mmh3:
1331
                hash_value = sha1
1332
                algorithm = 'sha1'
1333
            elif mmh3 and not md5 and not sha1:
1334
                hash_value = mmh3
1335
                algorithm = 'mmh3'
1336
            else:
1337
                raise TypeError("Conflicting values passed for hash/algorithm")
1338
        elif not hash_value or not algorithm:
1339
            raise TypeError("Missing values for hash_value/algorithm")
1340
1341
        url = self.__build_url('analysis', ['query/file_hash'])
1342
        params = purge_none({
1343
            'hash_value': hash_value,
1344
            'hash_algorithm': algorithm,
1345
            'hash_block_size': block_size,
1346
          })
1347
        return self._api_request(url, params, raw=raw, post=True)
1348
1349
    def is_blocked_file_hash(self, hash_value=None, algorithm=None,
1350
                             block_size=None, md5=None, sha1=None, mmh3=None,
1351
                             raw=False):
1352
        """
1353
        Check if the given file-hash belongs to a malicious file and we have
1354
        gathered enough information to block based on this (partial) hash.
1355
1356
        :param hash_value: The (partial) file-hash.
1357
        :param algorithm: One of MD5/SHA1/MMH3
1358
        :param block_size: Size of the block (at file start) used for generating
1359
            the hash-value. By default (or if 0), the entire file is assumed.
1360
        :param md5: Helper to quickly set `hash_value` and `algorithm`
1361
        :param sha1: Helper to quickly set `hash_value` and `algorithm`
1362
        :param mmh3: Helper to quickly set `hash_value` and `algorithm`
1363
        :param raw: if True, return the raw JSON/XML results of the API query.
1364
        :param requested_format: JSON or XML. If format is not JSON, this implies `raw`.
1365
        """
1366
        if md5 or sha1 or mmh3:
1367
            if hash_value or algorithm:
1368
                raise TypeError("Conflicting values passed for hash/algorithm")
1369
            if md5 and not sha1 and not mmh3:
1370
                hash_value = md5
1371
                algorithm = 'md5'
1372
            elif sha1 and not md5 and not mmh3:
1373
                hash_value = sha1
1374
                algorithm = 'sha1'
1375
            elif mmh3 and not md5 and not sha1:
1376
                hash_value = mmh3
1377
                algorithm = 'mmh3'
1378
            else:
1379
                raise TypeError("Conflicting values passed for hash/algorithm")
1380
        elif not hash_value or not algorithm:
1381
            raise TypeError("Missing values for hash_value/algorithm")
1382
1383
        url = self.__build_url('analysis', ['query/is_blocked_file_hash'])
1384
        params = purge_none({
1385
            'hash_value': hash_value,
1386
            'hash_algorithm': algorithm,
1387
            'hash_block_size': block_size,
1388
          })
1389
        return self._api_request(url, params, raw=raw, post=True)
1390
1391
    def query_analysis_engine_tasks(self, analysis_engine_task_uuids,
1392
                                    analysis_engine='analyst', raw=False):
1393
        """
1394
        Provide a set of task UUIDs from an analysis engine (such as Analyst
1395
        Scheduler or Anubis) and find completed tasks that contain this analysis
1396
        engine task.
1397
1398
        For return values and error codes please
1399
        see :py:meth:`malscape.api.views.analysis.query_analysis_engine_tasks`.
1400
1401
        If there is an error and `raw` is not set,
1402
        a :py:class:`AnalysisAPIError` exception will be raised.
1403
1404
        :param analysis_engine_task_uuids: List of analysis engine task UUIDs to
1405
            search.
1406
        :param analysis_engine: The analysis engine the task refers to.
1407
        :param raw: if True, return the raw JSON results of the API query.
1408
        """
1409
        url = self.__build_url('analysis', ['query/analysis_engine_tasks'])
1410
        params = purge_none({
1411
            'analysis_engine_task_uuids': ','.join(analysis_engine_task_uuids),
1412
            'analysis_engine': analysis_engine,
1413
        })
1414
        return self._api_request(url, params, post=True, raw=raw)
1415
1416
    def analyze_sandbox_result(self, analysis_task_uuid,
1417
                               analysis_engine='anubis',
1418
                               full_report_score=None,
1419
                               bypass_cache=False,
1420
                               raw=False):
1421
        """
1422
        Provide a task UUID from an analysis engine (such as Analyst Scheduler
1423
        or Anubis) and trigger scoring of the activity captured by the analysis
1424
        report.
1425
1426
        Similar to submitting by exe hash (md5/sha1) but we can enforce
1427
        the precise analysis result (if there are multiple) that we want
1428
        to score
1429
1430
        For return values and error codes please
1431
        see :py:meth:`malscape.api.views.analysis.analyze_sandbox_result`.
1432
1433
        If there is an error and `raw` is not set,
1434
        a :py:class:`AnalysisAPIError` exception will be raised.
1435
1436
        :param analysis_task_uuid: The sandbox task UUID to analyze/import.
1437
        :param analysis_engine: The sandbox the task refers to.
1438
        :param full_report_score: if set, this value (between -1 and 101)
1439
            determines starting at which scores a full report is returned.
1440
            -1 and 101 indicate "never return full report";
1441
            0 indicates "return full report at all times"
1442
        :param bypass_cache: if True, the API will not serve a cached
1443
            result. NOTE: This requires special privileges.
1444
        :param raw: if True, return the raw JSON results of the API query.
1445
        """
1446
        url = self.__build_url('analysis', ['analyze_sandbox_result'])
1447
        params = {
1448
            'analysis_task_uuid':analysis_task_uuid,
1449
            'analysis_engine': analysis_engine,
1450
            'full_report_score': full_report_score,
1451
            'bypass_cache': bypass_cache and 1 or None,
1452
          }
1453
        purge_none(params)
1454
        return self._api_request(url, params, raw=raw)
1455
1456
    def _api_request(self,
1457
                     url,
1458
                     params=None,
1459
                     files=None,
1460
                     timeout=None,
1461
                     post=False,
1462
                     raw=False,
1463
                     requested_format="json",
1464
                     verify=True):
1465
        """
1466
        Send an API request and return the results.
1467
1468
        :param url: API URL to fetch.
1469
        :param params: GET or POST parameters.
1470
        :param files: files to upload with request.
1471
        :param timeout: request timeout in seconds.
1472
        :param post: use HTTP POST instead of GET
1473
        :param raw: return the raw json results of API query
1474
        :param requested_foramt: JSON or XML. If format is not JSON, this implies `raw`.
1475
        """
1476
        raise NotImplementedError("%s does not implement api_request()" % self.__class__.__name__)
1477
1478
    def _process_response_page(self, page, raw, requested_format):
1479
        """
1480
        Helper for formatting/processing api response before returning it.
1481
        """
1482
        if raw or requested_format.lower() != "json":
1483
            return page
1484
1485
        #why does pylint think result is a bool??
1486
        #pylint: disable=E1103
1487
        result = json.loads(page)
1488
        success = result['success']
1489
        if success:
1490
            return result
1491
        else:
1492
            error_code = result.get('error_code', None)
1493
            # raise the most specific error we can
1494
            exception_class = AnalysisClientBase.ERRORS.get(error_code) or \
1495
                              AnalysisAPIError
1496
            raise exception_class(result['error'], error_code)
1497
1498
    def rescore_task(self, uuid=None, md5=None, sha1=None,
1499
                     min_score=0, max_score=100,
1500
                     threat=None, threat_class=None,
1501
                     force_local=False, raw=False):
1502
        """
1503
        Enforce re-scoring of a specific task or multiple tasks based on the
1504
        submitted file. Requires specific permissions.
1505
1506
        At least one of uuid/md5 must be provided. If sha1 is given, it must
1507
        match with the md5 that was provided. Existing manual-score threat/
1508
        threat-class information will not be overwritten unless an empty-
1509
        string ('') is passed to this function.
1510
1511
        This API-call returns the task-UUIDs that were triggered for rescoring.
1512
1513
        NOTE: Even when a single task-UUID is passed, the API might decide to
1514
        re-score all tasks for the same file!
1515
1516
        :param uuid: the unique identifier of the submitted task,
1517
            as returned in the task_uuid field of submit methods.
1518
        :param md5: the md5 hash of the submitted file.
1519
        :param sha1: the sha1 hash of the submitted file.
1520
        :param force_local: if True, enforce that the manual score is applied
1521
            only locally. This is the default for on-premise instances and
1522
            cannot be enforced there. Requires special permissions.
1523
        :param raw: if True, return the raw JSON/XML results of the API query.
1524
        """
1525
        assert uuid or md5, "Please provide task-uuid/md5"
1526
        url = self.__build_url('management', ['rescore'])
1527
        params = purge_none({
1528
            'uuid': uuid,
1529
            'md5': md5,
1530
            'sha1': sha1,
1531
            'min_score': min_score,
1532
            'max_score': max_score,
1533
            'threat': threat,
1534
            'threat_class': threat_class,
1535
            # use the default if no force is set
1536
            'force_local': force_local and 1 or None,
1537
        })
1538
        return self._api_request(url, params, raw=raw, post=True)
1539
1540
    def rescore_scanner(self, scanner, after, before,
1541
                         min_score=0, max_score=100,
1542
                         min_scanner_score=0, max_scanner_score=100,
1543
                         max_version=None, test_flag=None, force=False,
1544
                         raw=False):
1545
        """
1546
        Find tasks that triggered a certain scanner and mark them for
1547
        reprocessing.
1548
1549
        This API-call returns the task-UUIDs that were triggered for rescoring.
1550
1551
        :param scanner: Name of the scanner.
1552
        :param after: Reprocess tasks completed after this time.
1553
        :param before: Reprocess tasks completed before this time.
1554
        :param min_score: Minimum score of tasks to reprocess.
1555
        :param max_score: Maximum score of tasks to reprocess.
1556
        :param min_scanner_score: Minimum score of scanner detection (on backend
1557
            task) to reprocess.
1558
        :param max_scanner_score: Maximum score of scanner detection (on backend
1559
            task) to reprocess.
1560
        :param max_version: Maximum version of scanner to reprocess.
1561
        :param test_flag: If True, only affect backend-tasks where the scanner
1562
            was in *test* mode; if False, only affect backend-tasks where the
1563
            scanner was in *real* mode; otherwise affect all backend-tasks
1564
            regardless of the *test* flag.
1565
        :param force: By default, the API will refuse rescoring any scanners that
1566
            affect more than 100 tasks. To rescore large amounts, distribute the
1567
            work over multiple time-windows. This safety can be disabled by
1568
            setting the *force* parameter to True.
1569
        """
1570
        if hasattr(before, "strftime"):
1571
            before = before.strftime(AnalysisClientBase.DATETIME_FMT)
1572
        if hasattr(after, "strftime"):
1573
            after = after.strftime(AnalysisClientBase.DATETIME_FMT)
1574
1575
        url = self.__build_url('management', ['rescore_scanner'])
1576
        params = purge_none({
1577
            'scanner': scanner,
1578
            'after': after,
1579
            'before': before,
1580
            'min_score': min_score,
1581
            'max_score': max_score,
1582
            'min_scanner_score': min_scanner_score,
1583
            'max_scanner_score': max_scanner_score,
1584
            'max_version': max_version,
1585
        })
1586
        if test_flag is not None:
1587
            params['test_flag'] = test_flag and 1 or 0
1588
        if force:
1589
            params['force'] = 1
1590
        return self._api_request(url, params, raw=raw, post=True)
1591
1592
    def suppress_scanner(self, scanner, max_version, raw=False):
1593
        """
1594
        Mark a scanner as suppressed.
1595
1596
        :param scanner: Name of the scanner.
1597
        :param max_version: Version of scanner up to which it is supposed to be
1598
            suppressed. So, if the first scanner-version that should be used
1599
            for scoring is X, provide (X-1).
1600
        """
1601
        url = self.__build_url('management', ['suppress_scanner'])
1602
        params = purge_none({
1603
            'scanner': scanner,
1604
            'max_version': max_version,
1605
        })
1606
        return self._api_request(url, params, raw=raw, post=True)
1607
1608
    def create_ticket(self, uuid=None, md5=None, sha1=None,
1609
                      min_score=0, max_score=100, summary=None, labels=None,
1610
                      is_false_negative=False, is_false_positive=False,
1611
                      is_from_customer=False, is_from_partner=False,
1612
                      force=False, raw=False):
1613
        """
1614
        Enforce re-scoring of a specific task or multiple tasks based on the
1615
        submitted file. Requires specific permissions.
1616
1617
        At least one of uuid/md5/sha1 must be provided. If both file-hashes are
1618
        provided, they must match the same file.
1619
1620
        :param uuid: the unique identifier of the submitted task,
1621
            as returned in the task_uuid field of submit methods.
1622
        :param md5: the md5 hash of the submitted file.
1623
        :param sha1: the sha1 hash of the submitted file.
1624
        :param force: if True, enforce the generation of a ticket, even if none
1625
            of the task-analysis rules would have generated a ticket
1626
        :param min_score: Limit generation of tickets to tasks above the given
1627
            threshold
1628
        :param max_score: Limit generation of tickets to tasks below the given
1629
            threshold
1630
        :param summary: Optional summary (title) to use for the ticket.
1631
        :param labels: Optional set of labels to assign to a task
1632
        :param is_false_negative: Helper parameter to add the standard FN label
1633
        :param is_false_positive: Helper parameter to add the standard FP label
1634
        :param is_from_customer: Helper parameter to add the standard
1635
            from-customer label
1636
        :param is_from_partner: Helper parameter to add the standard
1637
            from-partner label
1638
        :param raw: if True, return the raw JSON/XML results of the API query.
1639
        """
1640
        assert uuid or md5 or sha1, "Please provide task-uuid/md5/sha1"
1641
        url = self.__build_url('management', ['create_ticket'])
1642
        if labels:
1643
            labels = set(labels)
1644
        else:
1645
            labels = set()
1646
        if is_false_negative:
1647
            labels.add('false_negatives')
1648
        if is_false_positive:
1649
            labels.add('false_positives')
1650
        if is_from_customer:
1651
            labels.add('from-customer')
1652
        if is_from_partner:
1653
            labels.add('from-partner')
1654
        if labels:
1655
            labels_list = ','.join(labels)
1656
        else:
1657
            labels_list = None
1658
        params = purge_none({
1659
            'uuid': uuid,
1660
            'md5': md5,
1661
            'sha1': sha1,
1662
            'min_score': min_score,
1663
            'max_score': max_score,
1664
            'force': force and 1 or 0,
1665
            'summary': summary,
1666
            'labels': labels_list,
1667
        })
1668
        return self._api_request(url, params, raw=raw, post=True)
1669
1670
    # pylint: disable=W0613
1671
    # raw, query_end, query_start are unused
1672
    def get_license_activity(self, query_start=None, query_end=None,
1673
                             raw=False):
1674
        """
1675
        Fetch license activity information.
1676
1677
        DEPRECATED. DO NOT USE
1678
        """
1679
        assert False, "Call to deprecated API function"
1680
    # pylint: enable=W0613
1681
1682
    def get_detections(self, report_uuid, raw=False):
1683
        """
1684
        Retrieve full internal scoring details. Requires special permissions
1685
1686
        :param report_uuid: Backend-report UUID as returned by `get_result`
1687
        :returns: Dictionary with detailed detection information
1688
        """
1689
        url = self.__build_url('research', [ 'get_detections' ])
1690
        params = { 'report_uuid': report_uuid }
1691
        return self._api_request(url, params, raw=raw, post=True)
1692
1693
    def get_backend_scores(self, md5=None, sha1=None, raw=False):
1694
        """
1695
        Download detailed detection information for all backend results for a
1696
        file.
1697
1698
        :param md5: MD5 of the file to query
1699
        :param sha1: SHA1 of the file to query
1700
        :returns: Dictionary with detailed detection information
1701
        """
1702
        assert md5 or sha1, "Need to provide one of md5/sha1"
1703
        url = self.__build_url('research', [ 'get_backend_scores' ])
1704
        params = purge_none({
1705
            'file_md5': md5,
1706
            'file_sha1': sha1,
1707
        })
1708
        return self._api_request(url, params, raw=raw, post=True)
1709
1710
1711
class AnalysisClient(AnalysisClientBase):
1712
    """
1713
    Client for the Analysis API.
1714
1715
    A client for the Analysis API that accesses the API through the web,
1716
    using key and api token for authentication, and the python
1717
    requests module for sending requests.
1718
1719
    :param base_url: URL where the lastline analysis API is located. (required)
1720
    :param key: API key for the Lastline Analyst API (required)
1721
    :param api_token: API token for the Lastline Analyst API (required)
1722
    :param logger: if provided, should be a python logging.Logger object
1723
        or object with similar interface.
1724
    :param ca_bundle: if provided, location of Certification Authority bundle
1725
        to use for authentication. This should not be required
1726
        if certificates are properly setup on the system.
1727
    :param verify_ssl: if True, verify SSL certificates. This overrides the
1728
        per-call parameter
1729
    :param proxies: dictionay with per-protocol proxy to use to use
1730
        (e.g. { 'http': 'localhost:3128', 'https': 'localhost:3128' }
1731
    :param timeout: default timeout (in seconds) to use for network requests.
1732
        Set to None to disable timeouts
1733
    """
1734
    def __init__(self,
1735
                 base_url,
1736
                 key,
1737
                 api_token,
1738
                 logger=None,
1739
                 ca_bundle=None,
1740
                 verify_ssl=True,
1741
                 use_curl=False,
1742
                 timeout=60,
1743
                 proxies=None,
1744
                 config=None):
1745
        AnalysisClientBase.__init__(self, base_url, logger, config)
1746
        self.__key = key
1747
        self.__api_token = api_token
1748
        self.__ca_bundle = ca_bundle
1749
        self.__verify_ssl = verify_ssl
1750
        self.__logger = logger
1751
        self.__timeout = timeout
1752
        if use_curl and logger:
1753
            logger.warning("Ingoring deprecated use_curl option")
1754
        if proxies is None and config:
1755
            self.__proxies = get_proxies_from_config(config)
1756
        else:
1757
            self.__proxies = proxies
1758
        self.__session = requests.session()
1759
1760
    def set_key(self, key):
1761
        self.__key = key
1762
1763
    def _api_request(self,
1764
                     url,
1765
                     params=None,
1766
                     files=None,
1767
                     timeout=None,
1768
                     post=False,
1769
                     raw=False,
1770
                     requested_format="json",
1771
                     verify=True):
1772
        if self._logger():
1773
            self._logger().info("Requesting %s" % url)
1774
        if requested_format.lower() != "json":
1775
            raw = True
1776
        if not params:
1777
            params = {}
1778
        params["key"] = self.__key
1779
        # NOTE: certain functions allow access without an api-token. Then,
1780
        # a valid license-key is sufficient. We must not pass an invalid
1781
        # or empty, however
1782
        if self.__api_token:
1783
            params["api_token"] = self.__api_token
1784
        if self.REQUEST_PERFDATA:
1785
            # we allow anyone setting this flag, but only admins will get
1786
            # any data back
1787
            params['perfdata'] = 1
1788
1789
        method = "GET"
1790
        data = None
1791
        if post or files:
1792
            method = "POST"
1793
            data = params
1794
            params = None
1795
1796
        if not self.__verify_ssl or not verify:
1797
            verify_ca_bundle = False
1798
        elif self.__ca_bundle:
1799
            verify_ca_bundle = self.__ca_bundle
1800
        else:
1801
            verify_ca_bundle = True
1802
1803
        try:
1804
            response = self.__session.\
1805
                request(method, url,
1806
                        params=params, data=data, files=files,
1807
                        timeout=timeout or self.__timeout,
1808
                        verify=verify_ca_bundle,
1809
                        proxies=self.__proxies)
1810
            # raise if anything went wrong
1811
            response.raise_for_status()
1812
        except requests.RequestException, exc:
1813
            if self.__logger:
1814
                self.__logger.error("Error contacting Malscape API: %s", exc)
1815
            # raise a wrapped exception
1816
            raise CommunicationError(error=exc)
1817
1818
        # Get the response content, as a unicode string if the response is
1819
        # textual, as a regular string otherwise.
1820
        content_type = response.headers.get("content-type")
1821
        if content_type and \
1822
                (content_type.startswith("application/json") or
1823
                 content_type.startswith("text/")):
1824
            data = response.text
1825
        else:
1826
            data = response.content
1827
1828
        return self._process_response_page(data, raw, requested_format)
1829
1830
1831
def init_shell(banner):
1832
    """Set up the iPython shell."""
1833
    try:
1834
        #this import can fail, that's why it's in a try block!
1835
        #pylint: disable=E0611
1836
        #pylint: disable=F0401
1837
        from IPython.frontend.terminal.embed import InteractiveShellEmbed #@UnresolvedImport
1838
        #pylint: enable=E0611
1839
        #pylint: enable=F0401
1840
        shell = InteractiveShellEmbed(banner1=banner)
1841
    except ImportError: # iPython < 0.11
1842
        # iPython <0.11 does have a Shell member
1843
        shell = IPython.Shell.IPShellEmbed() #pylint: disable=E1101
1844
        shell.set_banner(banner)
1845
1846
    return shell
1847
1848
1849
BANNER = """
1850
--------------------------------------
1851
Lastline Analyst API shell
1852
--------------------------------------
1853
1854
The 'analysis' object is an AnalysisClient,
1855
which can be used to access the functionality
1856
of the lastline Analysis API.
1857
1858
This is an IPython shell, so you can take
1859
advantage of tab auto-completion and other
1860
convenient features of IPython.
1861
"""
1862
URL = "https://analysis.lastline.com"
1863
def main(argv):
1864
    parser = optparse.OptionParser(usage="""
1865
Run client for analysis api with the provided credentials
1866
1867
    %prog access_key api_token
1868
1869
""")
1870
    parser.add_option("-u", "--api-url", dest="api_url",
1871
        type="string", default=URL,
1872
        help="send API requests to this URL (debugging purposes)")
1873
1874
    (cmdline_options, args) = parser.parse_args(argv[1:])
1875
    if len(args) != 2:
1876
        parser.print_help()
1877
        return 1
1878
1879
    namespace = {}
1880
    namespace["analysis"] = AnalysisClient(cmdline_options.api_url,
1881
                                           key=args[0],
1882
                                           api_token=args[1])
1883
1884
    shell = init_shell(BANNER)
1885
    shell(local_ns=namespace, global_ns=namespace)
1886
1887
    return 0
1888
1889
1890
if __name__ == "__main__":
1891
    sys.exit(main(sys.argv))
1892