Total Complexity | 109 |
Total Lines | 1290 |
Duplicated Lines | 0 % |
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 |
||
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 | |||
1892 |