Merge branch 'main' of github.com:misp/pymisp

pull/1002/head
Christian Studer 2022-10-26 11:11:49 +02:00
commit 48095df026
15 changed files with 1394 additions and 766 deletions

17
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every weekday
interval: "daily"

74
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,74 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '21 10 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@ -16,12 +16,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
- name: Set up Python ${{matrix.python-version}} - name: Set up Python ${{matrix.python-version}}
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{matrix.python-version}} python-version: ${{matrix.python-version}}
@ -36,4 +36,4 @@ jobs:
poetry run mypy tests/testlive_comprehensive.py tests/test_mispevent.py tests/testlive_sync.py pymisp poetry run mypy tests/testlive_comprehensive.py tests/test_mispevent.py tests/testlive_sync.py pymisp
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3

View File

@ -2,11 +2,139 @@ Changelog
========= =========
v2.4.162.1 (2022-10-02)
-----------------------
Changes
~~~~~~~
- Bump deps and version. [Raphaël Vinot]
Fix LIEF vuln.
- Bump deps, objects. [Raphaël Vinot]
Fix
~~~
- Change DNS warning list test. [Raphaël Vinot]
v2.4.162 (2022-09-09)
---------------------
New
~~~
- Pass arbitrary headers to a PyMISP request. [Raphaël Vinot]
- Allow to force the timestamps in to_dict/to_json, even if a change was
made. [Raphaël Vinot]
Changes
~~~~~~~
- Bump changelog. [Raphaël Vinot]
- Bump version. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
- Add in sort/desc for sorting results and limit/page for pagination.
[Tom King]
- Improve documentation for add_attribute. [Raphaël Vinot]
Fix
~~~
- Missing place to update version. [Raphaël Vinot]
v2.4.160.1 (2022-08-09)
-----------------------
Changes
~~~~~~~
- Bump changelog. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
Fix
~~~
- Make keepalive configuration linux only. [Raphaël Vinot]
Bump deps
v2.4.160 (2022-08-05)
---------------------
New
~~~
- Enable TCP keepalive. [Raphaël Vinot]
Changes
~~~~~~~
- Bump deps. [Raphaël Vinot]
- Bump version, deps. [Raphaël Vinot]
- Improve warning on invalid template, bump deps. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
- Make mypy happy. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
- Add in test case. [Tom King]
- Add ability to filter by sharing group for RestSearch for MISP >=
v2.4.158. [Tom King]
Fix
~~~
- Delete sharing group after deleting the event. [Raphaël Vinot]
- Give more time to MISP to publish the events before searching.
[Raphaël Vinot]
- Improper json check on non-json responses. [Raphaël Vinot]
Fix #854
- Mark all attributes in a soft deleted object as soft deleted too.
[Raphaël Vinot]
Bump misp-objects, deps.
- Make flake8 happy. [Raphaël Vinot]
- Properly convert MSG to EML. [Raphaël Vinot]
- Update lock file. [Raphaël Vinot]
- [feed] fixes bug when template_uuid does not exist. [Christophe
Vandeplas]
Other
~~~~~
- Update api.py. [Derekt2]
- Fix typo in logging message. [Philipp Hauswirth]
- Fig: [feed] fixes bugs during export with old data. [Christophe
Vandeplas]
- Update pyproject.toml. [Steven]
Add publicsuffixlist optional package for URL Object, which has a more current list than pyfaup
- Fix multiple_space warning. [malvidin]
- Option to include more URLObject attributes Add publicsuffixlist faup
for URLObject Windows support URLObject with PSLFaup prefers IP to
host/domain. [malvidin]
- Ensure that keys are sorted in the returned `_to_feed()` dictionary.
[Yun Zheng Hu]
This allows for better deterministic feed output generation.
v2.4.159 (2022-05-30)
---------------------
New
~~~
- [example:copyTagsFromAttributesToEvent] Added script to copy tags from
attributes to the event level. [Sami Mokaddem]
Changes
~~~~~~~
- Bump version. [Raphaël Vinot]
- Bump deps. [Raphaël Vinot]
- Massive bump deps for python 3.7. [Raphaël Vinot]
v2.4.157 (2022-03-24) v2.4.157 (2022-03-24)
--------------------- ---------------------
Changes Changes
~~~~~~~ ~~~~~~~
- Bump object templates. [Raphaël Vinot]
- Bump changelog. [Raphaël Vinot]
- Bump changelog. [Raphaël Vinot] - Bump changelog. [Raphaël Vinot]
- Bump version. [Raphaël Vinot] - Bump version. [Raphaël Vinot]
- Bump deps, objects. [Raphaël Vinot] - Bump deps, objects. [Raphaël Vinot]

1514
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
__version__ = '2.4.159' __version__ = '2.4.162.1'
import logging import logging
import sys import sys
import warnings import warnings

View File

@ -280,6 +280,14 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
def __len__(self) -> int: def __len__(self) -> int:
return len([k for k in self.__dict__.keys() if not (k[0] == '_' or k in self.__not_jsonable)]) return len([k for k in self.__dict__.keys() if not (k[0] == '_' or k in self.__not_jsonable)])
@property
def force_timestamp(self) -> bool:
return self.__force_timestamps
@force_timestamp.setter
def force_timestamp(self, force: bool):
self.__force_timestamps = force
@property @property
def edited(self) -> bool: def edited(self) -> bool:
"""Recursively check if an object has been edited and update the flag accordingly """Recursively check if an object has been edited and update the flag accordingly

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import TypeVar, Optional, Tuple, List, Dict, Union, Any, Mapping, Iterable from typing import TypeVar, Optional, Tuple, List, Dict, Union, Any, Mapping, Iterable, MutableMapping
from datetime import date, datetime from datetime import date, datetime
import csv import csv
from pathlib import Path from pathlib import Path
@ -28,6 +28,18 @@ from .mispevent import MISPEvent, MISPAttribute, MISPSighting, MISPLog, MISPObje
MISPGalaxyCluster, MISPGalaxyClusterRelation, MISPCorrelationExclusion MISPGalaxyCluster, MISPGalaxyClusterRelation, MISPCorrelationExclusion
from .abstract import pymisp_json_default, MISPTag, AbstractMISP, describe_types from .abstract import pymisp_json_default, MISPTag, AbstractMISP, describe_types
if sys.platform == 'linux':
# Enable TCP keepalive by default on every requests
import socket
from urllib3.connection import HTTPConnection
HTTPConnection.default_socket_options = HTTPConnection.default_socket_options + [
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), # enable keepalive
(socket.SOL_TCP, socket.TCP_KEEPIDLE, 30), # Start pinging after 30s of idle time
(socket.SOL_TCP, socket.TCP_KEEPINTVL, 10), # ping every 10s
(socket.SOL_TCP, socket.TCP_KEEPCNT, 6) # kill the connection if 6 ping fail (60s total)
]
try: try:
# cached_property exists since Python 3.8 # cached_property exists since Python 3.8
from functools import cached_property # type: ignore from functools import cached_property # type: ignore
@ -138,11 +150,16 @@ class PyMISP:
:param cert: Client certificate, as described here: http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates :param cert: Client certificate, as described here: http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates
:param auth: The auth parameter is passed directly to requests, as described here: http://docs.python-requests.org/en/master/user/authentication/ :param auth: The auth parameter is passed directly to requests, as described here: http://docs.python-requests.org/en/master/user/authentication/
:param tool: The software using PyMISP (string), used to set a unique user-agent :param tool: The software using PyMISP (string), used to set a unique user-agent
:param http_headers: Arbitrary headers to pass to all the requests.
:param timeout: Timeout, as described here: https://requests.readthedocs.io/en/master/user/advanced/#timeouts :param timeout: Timeout, as described here: https://requests.readthedocs.io/en/master/user/advanced/#timeouts
""" """
def __init__(self, url: str, key: str, ssl: bool = True, debug: bool = False, proxies: Mapping = {}, def __init__(self, url: str, key: str, ssl: bool = True, debug: bool = False, proxies: Optional[MutableMapping[str, str]] = None,
cert: Tuple[str, tuple] = None, auth: AuthBase = None, tool: str = '', timeout: Optional[Union[float, Tuple[float, float]]] = None): cert: Optional[Union[str, Tuple[str, str]]] = None, auth: AuthBase = None, tool: str = '',
timeout: Optional[Union[float, Tuple[float, float]]] = None,
http_headers: Optional[Dict[str, str]]=None
):
if not url: if not url:
raise NoURL('Please provide the URL of your MISP instance.') raise NoURL('Please provide the URL of your MISP instance.')
if not key: if not key:
@ -151,14 +168,16 @@ class PyMISP:
self.root_url: str = url self.root_url: str = url
self.key: str = key self.key: str = key
self.ssl: bool = ssl self.ssl: bool = ssl
self.proxies: Mapping[str, str] = proxies self.proxies: Optional[MutableMapping[str, str]] = proxies
self.cert: Optional[Tuple[str, tuple]] = cert self.cert: Optional[Union[str, Tuple[str, str]]] = cert
self.auth: Optional[AuthBase] = auth self.auth: Optional[AuthBase] = auth
self.tool: str = tool self.tool: str = tool
self.timeout: Optional[Union[float, Tuple[float, float]]] = timeout self.timeout: Optional[Union[float, Tuple[float, float]]] = timeout
self.__session = requests.Session() # use one session to keep connection between requests self.__session = requests.Session() # use one session to keep connection between requests
if brotli_supported(): if brotli_supported():
self.__session.headers['Accept-Encoding'] = ', '.join(('br', 'gzip', 'deflate')) self.__session.headers['Accept-Encoding'] = ', '.join(('br', 'gzip', 'deflate'))
if http_headers:
self.__session.headers.update(http_headers)
self.global_pythonify = False self.global_pythonify = False
@ -176,7 +195,7 @@ class PyMISP:
pymisp_version_tup = tuple(int(x) for x in __version__.split('.')) pymisp_version_tup = tuple(int(x) for x in __version__.split('.'))
recommended_version_tup = tuple(int(x) for x in response['version'].split('.')) recommended_version_tup = tuple(int(x) for x in response['version'].split('.'))
if recommended_version_tup < pymisp_version_tup[:3]: if recommended_version_tup < pymisp_version_tup[:3]:
logger.info(f"The version of PyMISP recommended by the MISP instance (response['version']) is older than the one you're using now ({__version__}). If you have a problem, please upgrade the MISP instance or use an older PyMISP version.") logger.info(f"The version of PyMISP recommended by the MISP instance ({response['version']}) is older than the one you're using now ({__version__}). If you have a problem, please upgrade the MISP instance or use an older PyMISP version.")
elif pymisp_version_tup[:3] < recommended_version_tup: elif pymisp_version_tup[:3] < recommended_version_tup:
logger.warning(f"The version of PyMISP recommended by the MISP instance ({response['version']}) is newer than the one you're using now ({__version__}). Please upgrade PyMISP.") logger.warning(f"The version of PyMISP recommended by the MISP instance ({response['version']}) is newer than the one you're using now ({__version__}). Please upgrade PyMISP.")
@ -1172,6 +1191,17 @@ class PyMISP:
response = self._prepare_request('POST', 'taxonomies/update') response = self._prepare_request('POST', 'taxonomies/update')
return self._check_json_response(response) return self._check_json_response(response)
def set_taxonomy_required(self, taxonomy: Union[MISPTaxonomy, int, str], required: bool = False) -> Dict:
taxonomy_id = get_uuid_or_id_from_abstract_misp(taxonomy)
url = urljoin(self.root_url, 'taxonomies/toggleRequired/{}'.format(taxonomy_id))
payload = {
"Taxonomy": {
"required": required
}
}
response = self._prepare_request('POST', url, data=payload)
return self._check_json_response(response)
# ## END Taxonomies ### # ## END Taxonomies ###
# ## BEGIN Warninglists ### # ## BEGIN Warninglists ###
@ -2569,7 +2599,7 @@ class PyMISP:
return self._csv_to_dict(normalized_response_text) # type: ignore return self._csv_to_dict(normalized_response_text) # type: ignore
else: else:
return normalized_response_text return normalized_response_text
elif return_format in ['stix-xml', 'text']: elif return_format not in ['json', 'yara-json']:
return self._check_response(response) return self._check_response(response)
normalized_response = self._check_json_response(response) normalized_response = self._check_json_response(response)
@ -2647,6 +2677,10 @@ class PyMISP:
]] = None, ]] = None,
sharinggroup: Optional[List[SearchType]] = None, sharinggroup: Optional[List[SearchType]] = None,
minimal: Optional[bool] = None, minimal: Optional[bool] = None,
sort: Optional[str] = None,
desc: Optional[bool] = None,
limit: Optional[int] = None,
page: Optional[int] = None,
pythonify: Optional[bool] = None) -> Union[Dict, List[MISPEvent]]: pythonify: Optional[bool] = None) -> Union[Dict, List[MISPEvent]]:
"""Search event metadata shown on the event index page. Using ! in front of a value """Search event metadata shown on the event index page. Using ! in front of a value
means NOT, except for parameters date_from, date_to and timestamp which cannot be negated. means NOT, except for parameters date_from, date_to and timestamp which cannot be negated.
@ -2678,6 +2712,10 @@ class PyMISP:
:param publish_timestamp: Filter on event's publish timestamp. :param publish_timestamp: Filter on event's publish timestamp.
:param sharinggroup: Restrict by a sharing group | list :param sharinggroup: Restrict by a sharing group | list
:param minimal: Return only event ID, UUID, timestamp, sighting_timestamp and published. :param minimal: Return only event ID, UUID, timestamp, sighting_timestamp and published.
:param sort: The field to sort the events by, such as 'id', 'date', 'attribute_count'.
:param desc: Whether to sort events ascending (default) or descending.
:param limit: Limit the number of events returned
:param page: If a limit is set, sets the page to be returned. page 3, limit 100 will return records 201->300).
:param pythonify: Returns a list of PyMISP Objects instead of the plain json output. :param pythonify: Returns a list of PyMISP Objects instead of the plain json output.
Warning: it might use a lot of RAM Warning: it might use a lot of RAM
""" """
@ -2696,7 +2734,8 @@ class PyMISP:
query['timestamp'] = (self._make_timestamp(timestamp[0]), self._make_timestamp(timestamp[1])) query['timestamp'] = (self._make_timestamp(timestamp[0]), self._make_timestamp(timestamp[1]))
else: else:
query['timestamp'] = self._make_timestamp(timestamp) query['timestamp'] = self._make_timestamp(timestamp)
if query.get("sort"):
query["direction"] = "desc" if desc else "asc"
url = urljoin(self.root_url, 'events/index') url = urljoin(self.root_url, 'events/index')
response = self._prepare_request('POST', url, data=query) response = self._prepare_request('POST', url, data=query)
normalized_response = self._check_json_response(response) normalized_response = self._check_json_response(response)
@ -3514,7 +3553,8 @@ class PyMISP:
def _check_response(self, response: requests.Response, lenient_response_type: bool = False, expect_json: bool = False) -> Union[Dict, str]: def _check_response(self, response: requests.Response, lenient_response_type: bool = False, expect_json: bool = False) -> Union[Dict, str]:
"""Check if the response from the server is not an unexpected error""" """Check if the response from the server is not an unexpected error"""
if response.status_code >= 500: if response.status_code >= 500:
logger.critical(everything_broken.format(response.request.headers, response.request.body, response.text)) headers_without_auth = {i: response.request.headers[i] for i in response.request.headers if i != 'Authorization'}
logger.critical(everything_broken.format(headers_without_auth, response.request.body, response.text))
raise MISPServerError(f'Error code 500:\n{response.text}') raise MISPServerError(f'Error code 500:\n{response.text}')
if 400 <= response.status_code < 500: if 400 <= response.status_code < 500:
@ -3575,7 +3615,6 @@ class PyMISP:
# CakePHP params in URL # CakePHP params in URL
to_append_url = '/'.join([f'{k}:{v}' for k, v in kw_params.items()]) to_append_url = '/'.join([f'{k}:{v}' for k, v in kw_params.items()])
url = f'{url}/{to_append_url}' url = f'{url}/{to_append_url}'
req = requests.Request(request_type, url, data=d, params=params) req = requests.Request(request_type, url, data=d, params=params)
user_agent = f'PyMISP {__version__} - Python {".".join(str(x) for x in sys.version_info[:2])}' user_agent = f'PyMISP {__version__} - Python {".".join(str(x) for x in sys.version_info[:2])}'
if self.tool: if self.tool:

@ -1 +1 @@
Subproject commit db9d79b093d77e09ba1dcec36cfefc00379bf73c Subproject commit 06df3688900a24a43e101d39919d7a2c29d351ca

View File

@ -793,6 +793,12 @@ class MISPObject(AbstractMISP):
def _to_feed(self, with_distribution=False) -> Dict: def _to_feed(self, with_distribution=False) -> Dict:
if with_distribution: if with_distribution:
self._fields_for_feed.add('distribution') self._fields_for_feed.add('distribution')
if not hasattr(self, 'template_uuid'): # workaround for old events where the template_uuid was not yet mandatory
self.template_uuid = str(uuid.uuid5(uuid.UUID("9319371e-2504-4128-8410-3741cebbcfd3"), self.name))
if not hasattr(self, 'description'): # workaround for old events where description is not always set
self.description = '<unknown>'
if not hasattr(self, 'meta-category'): # workaround for old events where meta-category is not always set
setattr(self, 'meta-category', 'misc')
to_return = super(MISPObject, self)._to_feed() to_return = super(MISPObject, self)._to_feed()
if self.references: if self.references:
to_return['ObjectReference'] = [reference._to_feed() for reference in self.references] to_return['ObjectReference'] = [reference._to_feed() for reference in self.references]
@ -843,6 +849,7 @@ class MISPObject(AbstractMISP):
def delete(self): def delete(self):
"""Mark the object as deleted (soft delete)""" """Mark the object as deleted (soft delete)"""
self.deleted = True self.deleted = True
[a.delete() for a in self.attributes]
@property @property
def disable_validation(self): def disable_validation(self):
@ -995,8 +1002,16 @@ class MISPObject(AbstractMISP):
return all(relation in self._fast_attribute_access for relation in list_of_relations) return all(relation in self._fast_attribute_access for relation in list_of_relations)
def add_attribute(self, object_relation: str, simple_value: Optional[Union[str, int, float]] = None, **value) -> Optional[MISPAttribute]: def add_attribute(self, object_relation: str, simple_value: Optional[Union[str, int, float]] = None, **value) -> Optional[MISPAttribute]:
"""Add an attribute. object_relation is required and the value key is a """Add an attribute.
dictionary with all the keys supported by MISPAttribute""" :param object_relation: The object relation of the attribute you're adding to the object
:param simple_value: The value
:param value: dictionary with all the keys supported by MISPAttribute
Note: as long as PyMISP knows about the object template, only the object_relation and the simple_value are required.
If PyMISP doesn't know the template, you also **must** pass a type.
All the other options that can be passed along when creating an attribute (comment, IDS flag, ...)
will be either taked out of the template, or out of the default setting for the type as defined on the MISP instance.
"""
if simple_value is not None: # /!\ The value *can* be 0 if simple_value is not None: # /!\ The value *can* be 0
value['value'] = simple_value value['value'] = simple_value
if value.get('value') is None: if value.get('value') is None:
@ -1023,7 +1038,7 @@ class MISPObject(AbstractMISP):
attribute = MISPObjectAttribute(self._definition['attributes'][object_relation]) attribute = MISPObjectAttribute(self._definition['attributes'][object_relation])
else: else:
# Woopsie, this object_relation is unknown, no sane defaults for you. # Woopsie, this object_relation is unknown, no sane defaults for you.
logger.warning("The template ({}) doesn't have the object_relation ({}) you're trying to add.".format(self.name, object_relation)) logger.warning("The template ({}) doesn't have the object_relation ({}) you're trying to add. If you are creating a new event to push to MISP, please review your code so it matches the template.".format(self.name, object_relation))
attribute = MISPObjectAttribute({}) attribute = MISPObjectAttribute({})
else: else:
attribute = MISPObjectAttribute({}) attribute = MISPObjectAttribute({})

191
pymisp/tools/_psl_faup.py Normal file
View File

@ -0,0 +1,191 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import ipaddress
import socket
import idna
from publicsuffixlist import PublicSuffixList # type: ignore
from urllib.parse import urlparse, urlunparse
class UrlNotDecoded(Exception):
pass
class PSLFaup(object):
"""
Fake Faup Python Library using PSL for Windows support
"""
def __init__(self):
self.decoded = False
self.psl = PublicSuffixList()
self._url = None
self._retval = {}
self.ip_as_host = False
def _clear(self):
self.decoded = False
self._url = None
self._retval = {}
self.ip_as_host = False
def decode(self, url) -> None:
"""
This function creates a dict of all the url fields.
:param url: The URL to normalize
"""
self._clear()
if isinstance(url, bytes) and b'//' not in url[:10]:
url = b'//' + url
elif '//' not in url[:10]:
url = '//' + url
self._url = urlparse(url)
self.ip_as_host = False
hostname = _ensure_str(self._url.hostname)
try:
ipv4_bytes = socket.inet_aton(_ensure_str(hostname))
ipv4 = ipaddress.IPv4Address(ipv4_bytes)
self.ip_as_host = ipv4.compressed
except (OSError, ValueError):
try:
addr, _, _ = hostname.partition('%')
ipv6 = ipaddress.IPv6Address(addr)
self.ip_as_host = ipv6.compressed
except ValueError:
pass
self.decoded = True
self._retval = {}
@property
def url(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
netloc = self.get_host() + ('' if self.get_port() is None else ':{}'.format(self.get_port()))
return _ensure_bytes(
urlunparse(
(self.get_scheme(), netloc, self.get_resource_path(),
'', self.get_query_string(), self.get_fragment(),)
)
)
def get_scheme(self):
"""
Get the scheme of the url given in the decode function
:returns: The URL scheme
"""
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
return _ensure_str(self._url.scheme)
def get_credential(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if self._url.password:
return _ensure_str(self._url.username) + ':' + _ensure_str(self._url.password)
if self._url.username:
return _ensure_str(self._url.username)
def get_subdomain(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if self.get_host() is not None and not self.ip_as_host:
if self.get_domain() in self.get_host():
return self.get_host().rsplit(self.get_domain(), 1)[0].rstrip('.') or None
def get_domain(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if self.get_host() is not None and not self.ip_as_host:
return self.psl.privatesuffix(self.get_host())
def get_domain_without_tld(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if self.get_tld() is not None and not self.ip_as_host:
return self.get_domain().rsplit(self.get_tld(), 1)[0].rstrip('.')
def get_host(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if self._url.hostname is None:
return None
elif self._url.hostname.isascii():
return _ensure_str(self._url.hostname)
else:
return _ensure_str(idna.encode(self._url.hostname, uts46=True))
def get_unicode_host(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if not self.ip_as_host:
return idna.decode(self.get_host(), uts46=True)
def get_tld(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
if self.get_host() is not None and not self.ip_as_host:
return self.psl.publicsuffix(self.get_host())
def get_port(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
return self._url.port
def get_resource_path(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
return _ensure_str(self._url.path)
def get_query_string(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
return _ensure_str(self._url.query)
def get_fragment(self):
if not self.decoded:
raise UrlNotDecoded("You must call faup.decode() first")
return _ensure_str(self._url.fragment)
def get(self):
self._retval["scheme"] = self.get_scheme()
self._retval["tld"] = self.get_tld()
self._retval["domain"] = self.get_domain()
self._retval["domain_without_tld"] = self.get_domain_without_tld()
self._retval["subdomain"] = self.get_subdomain()
self._retval["host"] = self.get_host()
self._retval["port"] = self.get_port()
self._retval["resource_path"] = self.get_resource_path()
self._retval["query_string"] = self.get_query_string()
self._retval["fragment"] = self.get_fragment()
self._retval["url"] = self.url
return self._retval
def _ensure_bytes(binary) -> bytes:
if isinstance(binary, bytes):
return binary
else:
return binary.encode('utf-8')
def _ensure_str(string) -> str:
if isinstance(string, str):
return string
else:
return string.decode('utf-8')

View File

@ -6,14 +6,13 @@ import logging
import ipaddress import ipaddress
import email.utils import email.utils
from email import policy, message_from_bytes from email import policy, message_from_bytes
from email.utils import parsedate_to_datetime
from email.message import EmailMessage from email.message import EmailMessage
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Union, List, Tuple, Dict, cast from typing import Union, List, Tuple, Dict, cast, Any
from extract_msg import openMsg # type: ignore from extract_msg import openMsg
from extract_msg.message import Message as MsgObj # type: ignore from extract_msg.message import Message as MsgObj
from RTFDE.exceptions import MalformedEncapsulatedRtf, NotEncapsulatedRtf # type: ignore from RTFDE.exceptions import MalformedEncapsulatedRtf, NotEncapsulatedRtf # type: ignore
from RTFDE.deencapsulate import DeEncapsulator # type: ignore from RTFDE.deencapsulate import DeEncapsulator # type: ignore
from oletools.common.codepages import codepage2codec # type: ignore from oletools.common.codepages import codepage2codec # type: ignore
@ -101,10 +100,9 @@ class EMailObject(AbstractMISPObjectGenerator):
eml = self._build_eml(message, body, attachments) eml = self._build_eml(message, body, attachments)
return eml return eml
def _extract_msg_objects(self, msg_obj: MsgObj): def _extract_msg_objects(self, msg_obj: MsgObj) -> Tuple[EmailMessage, Dict, List[Any]]:
"""Extracts email objects needed to construct an eml from a msg.""" """Extracts email objects needed to construct an eml from a msg."""
original_eml_header = msg_obj._getStringStream('__substg1.0_007D') message: EmailMessage = email.message_from_string(msg_obj.header.as_string(), policy=policy.default) # type: ignore
message = email.message_from_string(original_eml_header, policy=policy.default)
body = {} body = {}
if msg_obj.body is not None: if msg_obj.body is not None:
body['text'] = {"obj": msg_obj.body, body['text'] = {"obj": msg_obj.body,
@ -276,9 +274,8 @@ class EMailObject(AbstractMISPObjectGenerator):
if headers: if headers:
self.add_attribute("header", "\n".join(headers)) self.add_attribute("header", "\n".join(headers))
if "Date" in message: if "Date" in message and message.get('date').datetime is not None:
self.add_attribute("send-date", self.add_attribute("send-date", message.get('date').datetime)
parsedate_to_datetime(message.get('date')))
if "To" in message: if "To" in message:
self.__add_emails("to", message["To"]) self.__add_emails("to", message["To"])

View File

@ -3,9 +3,13 @@
from .abstractgenerator import AbstractMISPObjectGenerator from .abstractgenerator import AbstractMISPObjectGenerator
import logging import logging
from pyfaup.faup import Faup # type: ignore
from urllib.parse import unquote_plus from urllib.parse import unquote_plus
try:
from pyfaup.faup import Faup # type: ignore
except (OSError, ImportError):
from ._psl_faup import PSLFaup as Faup
logger = logging.getLogger('pymisp') logger = logging.getLogger('pymisp')
faup = Faup() faup = Faup()
@ -13,8 +17,9 @@ faup = Faup()
class URLObject(AbstractMISPObjectGenerator): class URLObject(AbstractMISPObjectGenerator):
def __init__(self, url: str, **kwargs): def __init__(self, url: str, generate_all=False, **kwargs):
super().__init__('url', **kwargs) super().__init__('url', **kwargs)
self._generate_all = True if generate_all is True else False
faup.decode(unquote_plus(url)) faup.decode(unquote_plus(url))
self.generate_attributes() self.generate_attributes()
@ -24,3 +29,28 @@ class URLObject(AbstractMISPObjectGenerator):
self.add_attribute('host', value=faup.get_host()) self.add_attribute('host', value=faup.get_host())
if faup.get_domain(): if faup.get_domain():
self.add_attribute('domain', value=faup.get_domain()) self.add_attribute('domain', value=faup.get_domain())
if self._generate_all:
if hasattr(faup, 'ip_as_host') and faup.ip_as_host:
self.attributes = [attr for attr in self.attributes
if attr.object_relation not in ('host', 'domain')]
self.add_attribute('ip', value=faup.ip_as_host)
if faup.get_credential():
self.add_attribute('credential', value=faup.get_credential())
if faup.get_fragment():
self.add_attribute('fragment', value=faup.get_fragment())
if faup.get_port():
self.add_attribute('port', value=faup.get_port())
if faup.get_query_string():
self.add_attribute('query_string', value=faup.get_query_string())
if faup.get_resource_path():
self.add_attribute('resource_path', value=faup.get_resource_path())
if faup.get_scheme():
self.add_attribute('scheme', value=faup.get_scheme())
if faup.get_tld():
self.add_attribute('tld', value=faup.get_tld())
if faup.get_domain_without_tld():
self.add_attribute('domain_without_tld', value=faup.get_domain_without_tld())
if faup.get_subdomain():
self.add_attribute('subdomain', value=faup.get_subdomain())
if hasattr(faup, 'get_unicode_host') and faup.get_unicode_host() != faup.get_host():
self.add_attribute('text', value=faup.get_unicode_host())

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pymisp" name = "pymisp"
version = "2.4.159" version = "2.4.162.1"
description = "Python API for MISP." description = "Python API for MISP."
authors = ["Raphaël Vinot <raphael.vinot@circl.lu>"] authors = ["Raphaël Vinot <raphael.vinot@circl.lu>"]
license = "BSD-2-Clause" license = "BSD-2-Clause"
@ -42,24 +42,25 @@ include = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
requests = "^2.27.1" requests = "^2.28.1"
python-dateutil = "^2.8.2" python-dateutil = "^2.8.2"
jsonschema = "^4.6.0" jsonschema = "^4.16.0"
deprecated = "^1.2.13" deprecated = "^1.2.13"
extract_msg = {version = "^0.33.0", optional = true} extract_msg = {version = "^0.36.4", optional = true}
RTFDE = {version = "^0.0.2", optional = true} RTFDE = {version = "^0.0.2", optional = true}
oletools = {version = "^0.60.1", optional = true} oletools = {version = "^0.60.1", optional = true}
python-magic = {version = "^0.4.27", optional = true} python-magic = {version = "^0.4.27", optional = true}
pydeep2 = {version = "^0.5.1", optional = true} pydeep2 = {version = "^0.5.1", optional = true}
lief = {version = "^0.12.1", optional = true} lief = {version = "^0.12.2", optional = true}
beautifulsoup4 = {version = "^4.11.1", optional = true} beautifulsoup4 = {version = "^4.11.1", optional = true}
validators = {version = "^0.20.0", optional = true} validators = {version = "^0.20.0", optional = true}
sphinx-autodoc-typehints = {version = "^1.18.2", optional = true} sphinx-autodoc-typehints = {version = "^1.19.4", optional = true}
recommonmark = {version = "^0.7.1", optional = true} recommonmark = {version = "^0.7.1", optional = true}
reportlab = {version = "^3.6.10", optional = true} reportlab = {version = "^3.6.11", optional = true}
pyfaup = {version = "^1.2", optional = true} pyfaup = {version = "^1.2", optional = true}
chardet = {version = "^4.0.0", optional = true} publicsuffixlist = {version = "^0.9.0", optional = true}
urllib3 = {extras = ["brotli"], version = "^1.26.9", optional = true} chardet = {version = "^5.0.0", optional = true}
urllib3 = {extras = ["brotli"], version = "^1.26.12", optional = true}
[tool.poetry.extras] [tool.poetry.extras]
fileobjects = ['python-magic', 'pydeep2', 'lief'] fileobjects = ['python-magic', 'pydeep2', 'lief']
@ -71,17 +72,17 @@ url = ['pyfaup', 'chardet']
email = ['extract_msg', "RTFDE", "oletools"] email = ['extract_msg', "RTFDE", "oletools"]
brotli = ['urllib3'] brotli = ['urllib3']
[tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies]
requests-mock = "^1.9.3" requests-mock = "^1.10.0"
mypy = "^0.961" mypy = "^0.982"
ipython = "^7.34.0" ipython = "^7.34.0"
jupyterlab = "^3.4.3" jupyterlab = "^3.4.8"
types-requests = "^2.27.30" types-requests = "^2.28.11.1"
types-python-dateutil = "^2.8.17" types-python-dateutil = "^2.8.19"
types-redis = "^4.2.6" types-redis = "^4.3.21.1"
types-Flask = "^1.1.6" types-Flask = "^1.1.6"
pytest-cov = "^3.0.0" pytest-cov = "^4.0.0"
[build-system] [build-system]
requires = ["poetry_core>=1.0", "setuptools"] requires = ["poetry_core>=1.1", "setuptools"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View File

@ -43,7 +43,7 @@ try:
except ImportError as e: except ImportError as e:
print(e) print(e)
url = 'https://localhost:8443' url = 'https://localhost:8443'
key = 'i8ckGjsyrfRSCPqE0qqr0XJbsLlfbOyYDzdSDawM' key = 'sL9hrjIyY405RyGQHLx5DoCAM92BNmmGa8P4ck1E'
verifycert = False verifycert = False
@ -300,6 +300,35 @@ class TestComprehensive(unittest.TestCase):
self.admin_misp_connector.delete_event(second) self.admin_misp_connector.delete_event(second)
self.admin_misp_connector.delete_event(third) self.admin_misp_connector.delete_event(third)
def test_search_index(self):
try:
first, second, third = self.environment()
# Search as admin
events = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), pythonify=True)
self.assertEqual(len(events), 3)
for e in events:
self.assertIn(e.id, [first.id, second.id, third.id])
# Test limit and pagination
event_one = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), limit=1, page=1, pythonify=True)[0]
event_two = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), limit=1, page=2, pythonify=True)[0]
self.assertTrue(event_one.id != event_two.id)
two_events = self.admin_misp_connector.search_index(limit=2)
self.assertTrue(len(two_events), 2)
# Test ordering by the Info field. Can't use timestamp as each will likely have the same
event = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), sort="info", desc=True, limit=1, pythonify=True)[0]
# First|Second|*Third* event
self.assertEqual(event.id, third.id)
# *First*|Second|Third event
event = self.admin_misp_connector.search_index(timestamp=first.timestamp.timestamp(), sort="info", desc=False, limit=1, pythonify=True)[0]
self.assertEqual(event.id, first.id)
finally:
# Delete event
self.admin_misp_connector.delete_event(first)
self.admin_misp_connector.delete_event(second)
self.admin_misp_connector.delete_event(third)
def test_search_objects(self): def test_search_objects(self):
'''Search for objects''' '''Search for objects'''
try: try:
@ -889,7 +918,7 @@ class TestComprehensive(unittest.TestCase):
# Test PyMISP.add_attribute with enforceWarninglist enabled # Test PyMISP.add_attribute with enforceWarninglist enabled
_e = events[0] _e = events[0]
_a = _e.add_attribute('ip-src', '1.1.1.1', enforceWarninglist=True) _a = _e.add_attribute('ip-src', '8.8.8.8', enforceWarninglist=True)
_a = self.user_misp_connector.add_attribute(_e, _a) _a = self.user_misp_connector.add_attribute(_e, _a)
self.assertTrue('trips over a warninglist and enforceWarninglist is enforced' in _a['errors'][1]['errors'], _a) self.assertTrue('trips over a warninglist and enforceWarninglist is enforced' in _a['errors'][1]['errors'], _a)
@ -1096,6 +1125,7 @@ class TestComprehensive(unittest.TestCase):
first.attributes[0].to_ids = True first.attributes[0].to_ids = True
first = self.user_misp_connector.update_event(first) first = self.user_misp_connector.update_event(first)
self.admin_misp_connector.publish(first, alert=False) self.admin_misp_connector.publish(first, alert=False)
time.sleep(5)
csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp()) csv = self.user_misp_connector.search(return_format='csv', publish_timestamp=first.timestamp.timestamp())
self.assertEqual(len(csv), 1) self.assertEqual(len(csv), 1)
self.assertEqual(csv[0]['value'], first.attributes[0].value) self.assertEqual(csv[0]['value'], first.attributes[0].value)
@ -1164,6 +1194,7 @@ class TestComprehensive(unittest.TestCase):
try: try:
first = self.user_misp_connector.add_event(first) first = self.user_misp_connector.add_event(first)
self.admin_misp_connector.publish(first) self.admin_misp_connector.publish(first)
time.sleep(5)
text = self.user_misp_connector.search(return_format='text', eventid=first.id) text = self.user_misp_connector.search(return_format='text', eventid=first.id)
self.assertEqual('8.8.8.8', text.strip()) self.assertEqual('8.8.8.8', text.strip())
finally: finally:
@ -1626,6 +1657,16 @@ class TestComprehensive(unittest.TestCase):
r = self.admin_misp_connector.disable_taxonomy(tax) r = self.admin_misp_connector.disable_taxonomy(tax)
self.assertEqual(r['message'], 'Taxonomy disabled') self.assertEqual(r['message'], 'Taxonomy disabled')
# Test toggling the required status
r = self.admin_misp_connector.set_taxonomy_required(tax, not tax.required)
self.assertEqual(r['message'], 'Taxonomy toggleRequireded')
updatedTax = self.admin_misp_connector.get_taxonomy(tax, pythonify=True)
self.assertFalse(tax.required == updatedTax.required)
# Return back to default required status
r = self.admin_misp_connector.set_taxonomy_required(tax, not tax.required)
def test_warninglists(self): def test_warninglists(self):
# Make sure we're up-to-date # Make sure we're up-to-date
r = self.admin_misp_connector.update_warninglists() r = self.admin_misp_connector.update_warninglists()
@ -2234,6 +2275,7 @@ class TestComprehensive(unittest.TestCase):
self.assertEqual(sharing_group.sgorgs[0].org_id, self.test_org.id) self.assertEqual(sharing_group.sgorgs[0].org_id, self.test_org.id)
finally: finally:
self.admin_misp_connector.delete_sharing_group(sharing_group.id) self.admin_misp_connector.delete_sharing_group(sharing_group.id)
self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group))
def test_sharing_group_search(self): def test_sharing_group_search(self):
# Add sharing group # Add sharing group
@ -2277,8 +2319,10 @@ class TestComprehensive(unittest.TestCase):
# We should not be missing any of the attributes # We should not be missing any of the attributes
self.assertFalse(attribute_ids.difference(searched_attribute_ids)) self.assertFalse(attribute_ids.difference(searched_attribute_ids))
finally: finally:
self.admin_misp_connector.delete_sharing_group(sharing_group.id)
self.user_misp_connector.delete_event(event.id) self.user_misp_connector.delete_event(event.id)
self.admin_misp_connector.delete_sharing_group(sharing_group.id)
self.assertFalse(self.admin_misp_connector.sharing_group_exists(sharing_group))
def test_feeds(self): def test_feeds(self):
# Add # Add