Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-passivetotal for
openSUSE:Factory checked in at 2021-05-23 00:06:08
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-passivetotal (Old)
and /work/SRC/openSUSE:Factory/.python-passivetotal.new.2988 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-passivetotal"
Sun May 23 00:06:08 2021 rev:7 rq:894928 version:2.4.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-passivetotal/python-passivetotal.changes
2021-04-24 23:10:16.987490681 +0200
+++
/work/SRC/openSUSE:Factory/.python-passivetotal.new.2988/python-passivetotal.changes
2021-05-23 00:06:11.882603217 +0200
@@ -1,0 +2,20 @@
+Fri May 14 14:04:15 UTC 2021 - Sebastian Wagner <[email protected]>
+
+- update to version 2.4.0:
+ - Enhancements:
+ - Early implementation of exception handling for SSL properties; analyzer.
AnalyzerError now available as a base exception type.
+ - SSL certs will now populate their own ip property, accessing the SSL
history API when needed to fill in the details.
+ - New iphistory property of SSL certs to support the ip property and give
direct access to the historial results.
+ - Used the tldextract Python library to expose useful properties on Hostname
objects such as tld, registered_domain, and subdomain
+ - Change default days back for date-aware searches to 90 days (was 30)
+ - Reject IPs as strings for Hostname objects
+ - Ensure IPs are used when instantiating IPAddress objects
+ - Defang hostnames (i.e. analyzer.Hostname('api[.]riskiq[.]net') )
+ - Support for Articles as a property of Hostnames and IPs, with autoloading
for detailed fields including indicators, plus easy access to a list of all
articles directly from analyzer.AllArticles()
+ - Support for Malware as a property of Hostnames and IPs
+ - Better coverage of pretty printing and dictionary representation across
analyzer objects.
+ - Bug Fixes:
+ - Exception handling when no details found for an SSL certificate.
+ - Proper handling of None types that may have prevented result caching
+
+-------------------------------------------------------------------
Old:
----
passivetotal-2.3.0.tar.gz
New:
----
passivetotal-2.4.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-passivetotal.spec ++++++
--- /var/tmp/diff_new_pack.le90P8/_old 2021-05-23 00:06:12.334600812 +0200
+++ /var/tmp/diff_new_pack.le90P8/_new 2021-05-23 00:06:12.338600790 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%bcond_without test
Name: python-passivetotal
-Version: 2.3.0
+Version: 2.4.0
Release: 0
Summary: Client for the PassiveTotal REST API
License: GPL-2.0-only
@@ -34,6 +34,7 @@
Requires: python-future
Requires: python-python-dateutil
Requires: python-requests
+Requires: python-tldextract
Requires(post): update-alternatives
Requires(postun):update-alternatives
BuildArch: noarch
++++++ passivetotal-2.3.0.tar.gz -> passivetotal-2.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/PKG-INFO
new/passivetotal-2.4.0/PKG-INFO
--- old/passivetotal-2.3.0/PKG-INFO 2021-04-14 23:18:17.000000000 +0200
+++ new/passivetotal-2.4.0/PKG-INFO 2021-05-10 18:45:03.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: passivetotal
-Version: 2.3.0
+Version: 2.4.0
Summary: Library for the RiskIQ PassiveTotal and Illuminate API
Home-page: https://github.com/passivetotal/python_api
Author: RiskIQ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/__init__.py
new/passivetotal-2.4.0/passivetotal/analyzer/__init__.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/__init__.py 2021-04-14
23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/__init__.py 2021-05-10
18:44:25.000000000 +0200
@@ -3,8 +3,9 @@
from collections import namedtuple
from datetime import datetime, timezone, timedelta
from passivetotal import *
+from passivetotal.analyzer._common import AnalyzerError, is_ip
-DEFAULT_DAYS_BACK = 30
+DEFAULT_DAYS_BACK = 90
api_clients = {}
config = {
@@ -67,6 +68,23 @@
return config[key]
return config
+def get_object(input, type=None):
+ """Get an Analyzer object for a given input and type. If no type is
specified,
+ type will be autodetected based on the input.
+
+ Returns :class:`analyzer.Hostname` or :class:`analyzer.IPAddress`.
+ """
+ objs = {
+ 'IPAddress': IPAddress,
+ 'Hostname': Hostname
+ }
+ if type is None:
+ type = 'IPAddress' if is_ip(input) else 'Hostname'
+ elif type not in objs.keys():
+ raise AnalyzerError('type must be IPAddress or Hostname')
+ return objs[type](input)
+
+
def set_date_range(days_back=DEFAULT_DAYS_BACK, start=None, end=None):
"""Set a range of dates for all date-bounded API queries.
@@ -135,4 +153,5 @@
from passivetotal.analyzer.hostname import Hostname
from passivetotal.analyzer.ip import IPAddress
-from passivetotal.analyzer.ssl import CertificateField
\ No newline at end of file
+from passivetotal.analyzer.ssl import CertificateField
+from passivetotal.analyzer.articles import AllArticles
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/_common.py
new/passivetotal-2.4.0/passivetotal/analyzer/_common.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/_common.py 2021-04-14
23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/_common.py 2021-05-10
18:44:25.000000000 +0200
@@ -1,10 +1,19 @@
"""Base classes and common methods for the analyzer package."""
-
+import pprint
from datetime import datetime
+import re
+
+def is_ip(test):
+ """Test to see if a string contains an IPv4 address."""
+ pattern =
re.compile(r"(\d{1,3}(?:\.|\]\.\[|\[\.\]|\(\.\)|{\.})\d{1,3}(?:\.|\]\.\[|\[\.\]|\(\.\)|{\.})\d{1,3}(?:\.|\]\.\[|\[\.\]|\(\.\)|{\.})\d{1,3})")
+ return len(pattern.findall(test)) > 0
+def refang(hostname):
+ """Remove square braces around dots in a hostname."""
+ return re.sub(r'[\[\]]','', hostname)
class RecordList:
@@ -221,3 +230,40 @@
"""
return len(self) < self._totalrecords
+
+class PrettyRecord:
+ """A record that can pretty-print itself.
+
+ For best results, wrap this property in a print() statement.
+
+ Depends on a as_dict property on the base object.
+ """
+
+ @property
+ def pretty(self):
+ """Pretty printed version of this record."""
+ from passivetotal.analyzer import get_config
+ config = get_config('pprint')
+ return pprint.pformat(self.as_dict, **config)
+
+
+
+class PrettyList:
+ """A record list that can pretty-print itself.
+
+ Depends on an as_dict property each object in the list.
+ """
+
+ @property
+ def pretty(self):
+ """Pretty printed version of this record list."""
+ from passivetotal.analyzer import get_config
+ config = get_config('pprint')
+ return pprint.pformat([r.as_dict for r in self], **config)
+
+
+
+
+class AnalyzerError(Exception):
+ """Base error class for Analyzer objects."""
+ pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/articles.py
new/passivetotal-2.4.0/passivetotal/analyzer/articles.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/articles.py 1970-01-01
01:00:00.000000000 +0100
+++ new/passivetotal-2.4.0/passivetotal/analyzer/articles.py 2021-05-10
18:44:25.000000000 +0200
@@ -0,0 +1,277 @@
+from datetime import datetime, timezone
+from passivetotal.analyzer._common import (
+ RecordList, Record, FirstLastSeen
+)
+from passivetotal.analyzer import get_api
+
+
+
+class ArticlesList(RecordList):
+ """List of threat intelligence articles.
+
+ Contains a list of :class:`passivetotal.analyzer.articles.Article` objects.
+ """
+
+ def _get_shallow_copy_fields(self):
+ return ['_totalrecords']
+
+ def _get_sortable_fields(self):
+ return ['age','title','type']
+
+ def parse(self, api_response):
+ """Parse an API response."""
+ self._totalrecords = api_response.get('totalRecords')
+ self._records = []
+ for article in api_response.get('articles', []):
+ self._records.append(Article(article))
+
+ def filter_tags(self, tags):
+ """Filtered article list that includes articles with an exact match to
one
+ or more tags.
+
+ Tests the `match_tags` method on each article.
+
+ :param tags: String with one or multiple comma-separated tags, or a
list
+ :rtype: :class:`passivetotal.analyzer.articles.ArticlesList`
+ """
+ filtered_results = self._make_shallow_copy()
+ filtered_results._records = filter(lambda r: r.match_tags(tags),
self._records)
+ return filtered_results
+
+ def filter_text(self, text, fields=['tags','title','summary']):
+ """Filtered article list that contain the text in one or more fields.
+
+ Searches tags, title and summary by default - set `fields` param to a
+ smaller list to narrow the search.
+
+ :param text: text to search for
+ :param fields: list of fields to search (optional)
+ :rtype: :class:`passivetotal.analyzer.articles.ArticlesList`
+ """
+ filtered_results = self._make_shallow_copy()
+ filtered_results._records = filter(lambda r: r.match_text(text,
fields), self._records)
+ return filtered_results
+
+
+
+class AllArticles(ArticlesList):
+ """All threat intelligence articles currently published by RiskIQ.
+
+ Contains a list of :class:`passivetotal.analyzer.articles.Article` objects.
+
+ By default, instantiating the class will automatically load the entire list
+ of threat intelligence articles. Pass autoload=False to the constructor to
disable
+ this functionality.
+ """
+
+ def __init__(self, autoload = True):
+ """Initialize a list of articles; will autoload by default.
+
+ :param autoload: whether to automatically load articles upon
instantiation (defaults to true)
+ """
+ super().__init__()
+ if autoload:
+ self.load()
+
+ def load(self):
+ """Query the API for articles and load them into an articles list."""
+ response = get_api('Articles').get_articles()
+ self.parse(response)
+
+
+
+class Article(Record):
+ """A threat intelligence article."""
+
+ def __init__(self, api_response):
+ self._guid = api_response.get('guid')
+ self._title = api_response.get('title')
+ self._summary = api_response.get('summary')
+ self._type = api_response.get('type')
+ self._publishdate = api_response.get('publishDate')
+ self._link = api_response.get('link')
+ self._categories = api_response.get('categories')
+ self._tags = api_response.get('tags')
+ self._indicators = api_response.get('indicators')
+
+ def __str__(self):
+ return self.title
+
+ def __repr__(self):
+ return '<Article {}>'.format(self.guid)
+
+ def _api_get_details(self):
+ """Query the articles detail endpoint to fill in missing fields."""
+ response = get_api('Articles').get_details(self._guid)
+ self._summary = response.get('summary')
+ self._publishdate = response.get('publishedDate')
+ self._tags = response.get('tags')
+ self._categories = response.get('categories')
+ self._indicators = response.get('indicators')
+
+ def _ensure_details(self):
+ """Ensure we have details for this article.
+
+ Some API responses do not include full article details. This internal
method
+ will determine if they are missing and trigger an API call to fetch
them."""
+ if self._summary is None and self._publishdate is None:
+ self._api_get_details()
+
+ def _indicators_by_type(self, type):
+ """Get indicators of a specific type.
+
+ Indicators are grouped by type in the API response. This method finds
+ the group of a specified type and returns the dict of results directly
+ from the API response. It assumes there is only one instance of a group
+ type in the indicator list and therefore only returns the first one.
+ """
+ try:
+ return [ group for group in self.indicators if
group['type']==type][0]
+ except IndexError:
+ return {'type': None, 'count': 0, 'values': [] }
+
+ def match_tags(self, tags):
+ """Exact match search for one or more tags in this article's list of
tags.
+
+ :param tags: String with one or multiple comma-seperated tags, or a
list
+ :rtype bool: Whether any of the tags are included in this article's
list of tags.
+ """
+ if type(tags) is str:
+ tags = tags.split(',')
+ return len(set(tags) & set(self.tags)) > 0
+
+ def match_text(self, text, fields=['tags','title','summary']):
+ """Case insensitive substring search across article text fields.
+
+ Searches tags, title and summary by default - set `fields` param to a
+ smaller list to narrow the search.
+ :param text: text to search for
+ :param fields: list of fields to search (optional)
+ :rtype bool: whether the text was found in any of the fields
+ """
+ for field in ['title','summary']:
+ if field in fields:
+ if text.casefold() in getattr(self, field).casefold():
+ return True
+ if 'tags' in fields:
+ for tag in self.tags:
+ if text.casefold() in tag.casefold():
+ return True
+ return False
+
+ @property
+ def guid(self):
+ """Article unique ID within the RiskIQ system."""
+ return self._guid
+
+ @property
+ def title(self):
+ """Article short title."""
+ return self._title
+
+ @property
+ def type(self):
+ """Article visibility type (i.e. public, private)."""
+ return self._type
+
+ @property
+ def summary(self):
+ """Article summary."""
+ self._ensure_details()
+ return self._summary
+
+ @property
+ def date_published(self):
+ """Date the article was published, as a datetime object."""
+ self._ensure_details()
+ date = datetime.fromisoformat(self._publishdate)
+ return date
+
+ @property
+ def age(self):
+ """Age of the article in days."""
+ now = datetime.now(timezone.utc)
+ interval = now - self.date_published
+ return interval.days
+
+ @property
+ def link(self):
+ """URL to a page with article details."""
+ return self._link
+
+ @property
+ def categories(self):
+ """List of categories this article is listed in."""
+ self._ensure_details()
+ return self._categories
+
+ @property
+ def tags(self):
+ """List of tags attached to this article."""
+ self._ensure_details()
+ return self._tags
+
+ def has_tag(self, tag):
+ """Whether this article has a given tag."""
+ return (tag in self.tags)
+
+ @property
+ def indicators(self):
+ """List of indicators associated with this article.
+
+ This is the raw result retuned by the API. Expect an array of objects
each
+ representing a grouping of a particular type of indicator."""
+ self._ensure_details()
+ return self._indicators
+
+ @property
+ def indicator_count(self):
+ """Sum of all types of indicators in this article."""
+ return sum([i['count'] for i in self.indicators])
+
+ @property
+ def indicator_types(self):
+ """List of the types of indicators associated with this article."""
+ return [ group['type'] for group in self.indicators ]
+
+ @property
+ def ips(self):
+ """List of IP addresses in this article.
+
+ :rtype: :class:`passivetotal.analyzer.ip.IPAddress`
+ """
+ from passivetotal.analyzer import IPAddress
+ return [ IPAddress(ip) for ip in
self._indicators_by_type('ip')['values'] ]
+
+ @property
+ def hostnames(self):
+ """List of hostnames in this article.
+
+ :rtype: :class:`passivetotal.analyzer.ip.Hostname`
+ """
+ from passivetotal.analyzer import Hostname
+ return [ Hostname(domain) for domain in
self._indicators_by_type('domain')['values'] ]
+
+
+
+class HasArticles:
+
+ """An object which may be an indicator of compromise (IOC) published in an
Article."""
+
+ def _api_get_articles(self):
+ """Query the articles API for articles with this entity listed as an
indicator."""
+ response = get_api('Articles').get_articles_for_indicator(
+ self.get_host_identifier()
+ )
+ self._articles = ArticlesList(response)
+ return self._articles
+
+ @property
+ def articles(self):
+ """Threat intelligence articles that reference this host.
+
+ :rtype: :class:`passivetotal.analyzer.articles.ArticlesList`
+ """
+ if getattr(self, '_articles', None) is not None:
+ return self._articles
+ return self._api_get_articles()
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/passivetotal-2.3.0/passivetotal/analyzer/components.py
new/passivetotal-2.4.0/passivetotal/analyzer/components.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/components.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/components.py 2021-05-10
18:44:25.000000000 +0200
@@ -137,7 +137,7 @@
:rtype: :class:`passivetotal.analyzer.components.ComponentHistory`
"""
- if getattr(self, '_components'):
+ if getattr(self, '_components', None) is not None:
return self._components
config = get_config()
return self._api_get_components(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/cookies.py
new/passivetotal-2.4.0/passivetotal/analyzer/cookies.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/cookies.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/cookies.py 2021-05-10
18:44:25.000000000 +0200
@@ -109,7 +109,7 @@
:rtype: :class:`passivetotal.analyzer.components.CookieHistory`
"""
- if getattr(self, '_cookies'):
+ if getattr(self, '_cookies', None) is not None:
return self._cookies
config = get_config()
return self._api_get_cookies(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/enrich.py
new/passivetotal-2.4.0/passivetotal/analyzer/enrich.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/enrich.py 1970-01-01
01:00:00.000000000 +0100
+++ new/passivetotal-2.4.0/passivetotal/analyzer/enrich.py 2021-05-10
18:44:25.000000000 +0200
@@ -0,0 +1,104 @@
+from datetime import date
+from passivetotal.analyzer import get_api
+from passivetotal.analyzer._common import Record, RecordList, PrettyList,
PrettyRecord, AnalyzerError
+
+
+
+class MalwareList(RecordList, PrettyList):
+
+ """List of malware hashes associated with a host or domain."""
+
+ def __str__(self):
+ pass
+
+ def _get_shallow_copy_fields(self):
+ return []
+
+ def _get_sortable_fields(self):
+ return ['date_collected','source']
+
+ def parse(self, api_response):
+ """Parse an API response into a list of records."""
+ self._api_success = api_response.get('success',None)
+ self._records = []
+ for result in api_response.get('results',[]):
+ self._records.append(MalwareRecord(result))
+
+
+
+class MalwareRecord(Record, PrettyRecord):
+
+ """Record of malware associated with a host."""
+
+ def __init__(self, api_response):
+ self._date_collected = api_response.get('collectionDate')
+ self._sample = api_response.get('sample')
+ self._source = api_response.get('source')
+ self._source_url = api_response.get('sourceUrl')
+
+ def __str__(self):
+ return self.hash
+
+ def __repr__(self):
+ return "<MalwareRecord {0.hash}>".format(self)
+
+ @property
+ def as_dict(self):
+ """Malware record as a Python dictionary."""
+ return {
+ field: getattr(self, field) for field in [
+ 'hash','source','source_url','date_collected'
+ ]
+ }
+
+ @property
+ def hash(self):
+ """Hash of the malware sample."""
+ return self._sample
+
+ @property
+ def source(self):
+ """Source where the malware sample was obtained."""
+ return self._source
+
+ @property
+ def source_url(self):
+ """URL to malware sample source."""
+ return self._source_url
+
+ @property
+ def date_collected(self):
+ """Date the malware was collected, as a Python date object."""
+ try:
+ parsed = date.fromisoformat(self._date_collected)
+ except Exception:
+ raise AnalyzerError
+ return parsed
+
+
+
+class HasMalware:
+
+ """An object (ip or domain) with malware samples."""
+
+ def _api_get_malware(self):
+ """Query the enrichment API for malware samples."""
+ try:
+ response = get_api('Enrichment').get_malware(
+ query=self.get_host_identifier()
+ )
+ except Exception:
+ raise AnalyzerError('Error querying enrichment API for malware
samples')
+ self._malware = MalwareList(response)
+ return self._malware
+
+ @property
+ def malware(self):
+ """List of malware hashes associated with this host.
+
+ :rtype: :class:`passivetotal.analyzer.enrich.MalwareList`
+ """
+ if getattr(self, '_malware', None) is not None:
+ return self._malware
+ return self._api_get_malware()
+
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/hostname.py
new/passivetotal-2.4.0/passivetotal/analyzer/hostname.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/hostname.py 2021-04-14
23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/hostname.py 2021-05-10
18:44:25.000000000 +0200
@@ -1,21 +1,26 @@
"""Hostname analyzer for the RiskIQ PassiveTotal API."""
import socket
-from passivetotal.analyzer import get_api, get_config
-from passivetotal.analyzer.pdns import PdnsResolutions
-from passivetotal.analyzer.summary import HostnameSummary
+import tldextract
+from passivetotal.analyzer import get_api, get_config, get_object
+from passivetotal.analyzer._common import is_ip, refang, AnalyzerError
+from passivetotal.analyzer.pdns import HasResolutions
+from passivetotal.analyzer.summary import HostnameSummary, HasSummary
from passivetotal.analyzer.whois import DomainWhois
from passivetotal.analyzer.ssl import CertificateField
-from passivetotal.analyzer.ip import IPAddress
from passivetotal.analyzer.hostpairs import HasHostpairs
from passivetotal.analyzer.cookies import HasCookies
from passivetotal.analyzer.trackers import HasTrackers
from passivetotal.analyzer.components import HasComponents
from passivetotal.analyzer.illuminate import HasReputation
+from passivetotal.analyzer.articles import HasArticles
+from passivetotal.analyzer.enrich import HasMalware
-class Hostname(HasComponents, HasCookies, HasTrackers, HasHostpairs,
HasReputation):
+class Hostname(HasComponents, HasCookies, HasTrackers, HasHostpairs,
+ HasReputation, HasArticles, HasResolutions, HasSummary,
+ HasMalware):
"""Represents a hostname such as api.passivetotal.org.
@@ -31,21 +36,16 @@
def __new__(cls, hostname):
"""Create or find an instance for the given hostname."""
+ hostname = refang(hostname)
+ if is_ip(hostname):
+ raise AnalyzerError('Use analyzer.IPAddress for IPv4 addresses.')
self = cls._instances.get(hostname)
if self is None:
self = cls._instances[hostname] = object.__new__(Hostname)
self._hostname = hostname
- self._current_ip = None
- self._whois = None
- self._resolutions = None
- self._summary = None
- self._components = None
- self._cookies = None
- self._trackers = None
self._pairs = {}
self._pairs['parents'] = None
self._pairs['children'] = None
- self._reputation = None
return self
def __str__(self):
@@ -82,27 +82,13 @@
address as the query value.
"""
return self._hostname
-
- def _api_get_resolutions(self, unique=False, start_date=None,
end_date=None, timeout=None, sources=None):
- """Query the pDNS API for resolution history."""
- meth = get_api('DNS').get_unique_resolutions if unique else
get_api('DNS').get_passive_dns
- response = meth(
- query=self._hostname,
- start=start_date,
- end=end_date,
- timeout=timeout,
- sources=sources
- )
- self._resolutions = PdnsResolutions(response)
- return self._resolutions
-
+
def _api_get_summary(self):
"""Query the Cards API for summary data."""
- response = get_api('Cards').get_summary(query=self._hostname)
+ response =
get_api('Cards').get_summary(query=self.get_host_identifier())
self._summary = HostnameSummary(response)
return self._summary
-
def _api_get_whois(self, compact=False):
"""Query the Whois API for complete whois details."""
response = get_api('Whois').get_whois_details(query=self._hostname,
compact_record=compact)
@@ -112,9 +98,58 @@
def _query_dns(self):
"""Perform a DNS lookup."""
ip = socket.gethostbyname(self._hostname)
- self._current_ip = IPAddress(ip)
+ self._current_ip = get_object(ip,'IPAddress')
return self._current_ip
+ def _extract(self):
+ """Use the tldextract library to extract parts out of the hostname."""
+ self._tldextract = tldextract.extract(self._hostname)
+ return self._tldextract
+
+ @property
+ def domain(self):
+ """Returns only the domain portion of the registered domain name for
this hostname.
+
+ Uses the `tldextract` library and returns the domain property of the
+ `ExtractResults` named tuple.
+ """
+ if getattr(self, '_tldextract', None) is not None:
+ return self._tldextract.domain
+ return self._extract().domain
+
+ @property
+ def tld(self):
+ """Returns the top-level domain name (TLD) for this hostname.
+
+ Uses the `tldextract` library and returns the suffix property of the
+ `ExtractResults` named tuple.
+ """
+ if getattr(self, '_tldextract', None) is not None:
+ return self._tldextract.suffix
+ return self._extract().suffix
+
+ @property
+ def registered_domain(self):
+ """Returns the registered domain name (with TLD) for this hostname.
+
+ Uses the `tldextract` library and returns the registered_domain
property of the
+ `ExtractResults` named tuple.
+ """
+ if getattr(self, '_tldextract', None) is not None:
+ return self._tldextract.registered_domain
+ return self._extract().registered_domain
+
+ @property
+ def subdomain(self):
+ """Entire set of subdomains for this hostname (third level and higher).
+
+ Uses the `tldextract` library and returns the subdomain property of
the
+ `ExtractResults` named tuple.
+ """
+ if getattr(self, '_tldextract', None) is not None:
+ return self._tldextract.subdomain
+ return self._extract().subdomain
+
@property
def hostname(self):
"""Hostname as a string."""
@@ -128,32 +163,11 @@
:rtype: :class:`passivetotal.analyzer.IPAddress`
"""
- if getattr(self, '_current_ip'):
+ if getattr(self, '_current_ip', None) is not None:
return self._current_ip
return self._query_dns()
@property
- def resolutions(self):
- """ List of pDNS resolutions where hostname was the DNS query value.
-
- Bounded by dates set in :meth:`passivetotal.analyzer.set_date_range`.
-
- Provides list of :class:`passivetotal.analyzer.pdns.PdnsRecord`
objects.
-
- :rtype: :class:`passivetotal.analyzer.pdns.PdnsResolutions`
- """
- if getattr(self, '_resolutions'):
- return self._resolutions
- config = get_config()
- return self._api_get_resolutions(
- unique=False,
- start_date=config['start_date'],
- end_date=config['end_date'],
- timeout=config['pdns_timeout'],
- sources=config['pdns_sources']
- )
-
- @property
def certificates(self):
"""List of certificates where this hostname is contained in the
subjectAlternativeName field.
@@ -165,22 +179,12 @@
return CertificateField('subjectAlternativeName',
self._hostname).certificates
@property
- def summary(self):
- """Summary of PassiveTotal data available for this hostname.
-
- :rtype: :class:`passivetotal.analyzer.summary.HostnameSummary`
- """
- if getattr(self, '_summary'):
- return self._summary
- return self._api_get_summary()
-
- @property
def whois(self):
"""Most recently available Whois record for the hostname's domain name.
:rtype: :class:`passivetotal.analyzer.whois.DomainWhois`
"""
- if getattr(self, '_whois'):
+ if getattr(self, '_whois', None) is not None:
return self._whois
return self._api_get_whois(
compact=False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/passivetotal-2.3.0/passivetotal/analyzer/hostpairs.py
new/passivetotal-2.4.0/passivetotal/analyzer/hostpairs.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/hostpairs.py 2021-04-14
23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/hostpairs.py 2021-05-10
18:44:25.000000000 +0200
@@ -3,7 +3,7 @@
from passivetotal.analyzer._common import (
RecordList, Record, FirstLastSeen, PagedRecordList
)
-from passivetotal.analyzer import get_api, get_config
+from passivetotal.analyzer import get_api, get_config, get_object
@@ -101,14 +101,12 @@
@property
def child(self):
"""Descendant hostname for this pairing."""
- from passivetotal.analyzer import Hostname
- return Hostname(self._child)
+ return get_object(self._child)
@property
def parent(self):
"""Parent hostname for this pairing."""
- from passivetotal.analyzer import Hostname
- return Hostname(self._parent)
+ return get_object(self._parent)
@@ -144,7 +142,7 @@
:rtype: :class:`passivetotal.analyzer.hostpairs.HostpairHistory`
"""
- if self._pairs['parents']:
+ if self._pairs['parents'] is not None:
return self._pairs['parents']
config = get_config()
return self._api_get_hostpairs(
@@ -159,7 +157,7 @@
:rtype: :class:`passivetotal.analyzer.hostpairs.HostpairHistory`
"""
- if self._pairs['children']:
+ if self._pairs['children'] is not None:
return self._pairs['children']
config = get_config()
return self._api_get_hostpairs(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/passivetotal-2.3.0/passivetotal/analyzer/illuminate.py
new/passivetotal-2.4.0/passivetotal/analyzer/illuminate.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/illuminate.py 2021-04-14
23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/illuminate.py 2021-05-10
18:44:25.000000000 +0200
@@ -72,6 +72,6 @@
:rtype: :class:`passivetotal.analyzer.illuminate.ReputationScore`
"""
- if getattr(self, '_reputation'):
+ if getattr(self, '_reputation', None) is not None:
return self._reputation
return self._api_get_reputation()
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/ip.py
new/passivetotal-2.4.0/passivetotal/analyzer/ip.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/ip.py 2021-04-14
23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/ip.py 2021-05-10
18:44:25.000000000 +0200
@@ -2,19 +2,24 @@
from passivetotal.analyzer import get_api, get_config
-from passivetotal.analyzer.pdns import PdnsResolutions
+from passivetotal.analyzer._common import is_ip, AnalyzerError
+from passivetotal.analyzer.pdns import HasResolutions
from passivetotal.analyzer.services import Services
from passivetotal.analyzer.ssl import Certificates
-from passivetotal.analyzer.summary import IPSummary
+from passivetotal.analyzer.summary import IPSummary, HasSummary
from passivetotal.analyzer.hostpairs import HasHostpairs
from passivetotal.analyzer.cookies import HasCookies
from passivetotal.analyzer.trackers import HasTrackers
from passivetotal.analyzer.components import HasComponents
from passivetotal.analyzer.illuminate import HasReputation
+from passivetotal.analyzer.articles import HasArticles
+from passivetotal.analyzer.enrich import HasMalware
-class IPAddress(HasComponents, HasCookies, HasHostpairs, HasTrackers,
HasReputation):
+class IPAddress(HasComponents, HasCookies, HasHostpairs, HasTrackers,
+ HasReputation, HasArticles, HasResolutions, HasSummary,
+ HasMalware):
"""Represents an IPv4 address such as 8.8.8.8
@@ -29,22 +34,15 @@
def __new__(cls, ip):
"""Create or find an instance for the given IP."""
+ if not is_ip(ip):
+ raise AnalyzerError('Invalid IP address')
self = cls._instances.get(ip)
if self is None:
self = cls._instances[ip] = object.__new__(IPAddress)
self._ip = ip
- self._resolutions = None
- self._services = None
- self._ssl_history = None
- self._summary = None
- self._whois = None
- self._components = None
- self._cookies = None
- self._trackers = None
self._pairs = {}
self._pairs['parents'] = None
self._pairs['children'] = None
- self._reputation = None
return self
def __str__(self):
@@ -83,24 +81,17 @@
"""
return self._ip
- def _api_get_resolutions(self, unique=False, start_date=None,
end_date=None, timeout=None, sources=None):
- """Query the pDNS API for resolution history."""
- meth = get_api('DNS').get_unique_resolutions if unique else
get_api('DNS').get_passive_dns
- response = meth(
- query=self._ip,
- start=start_date,
- end=end_date,
- timeout=timeout,
- sources=sources
- )
- self._resolutions = PdnsResolutions(api_response=response)
- return self._resolutions
-
def _api_get_services(self):
"""Query the services API for service and port history."""
response = get_api('Services').get_services(query=self._ip)
self._services = Services(response)
return self._services
+
+ def _api_get_summary(self):
+ """Query the Cards API for summary data."""
+ response =
get_api('Cards').get_summary(query=self.get_host_identifier())
+ self._summary = IPSummary(response)
+ return self._summary
def _api_get_ssl_history(self):
"""Query the SSL API for certificate history."""
@@ -108,12 +99,6 @@
self._ssl_history = Certificates(response)
return self._ssl_history
- def _api_get_summary(self):
- """Query the Cards API for summary data."""
- response = get_api('Cards').get_summary(query=self._ip)
- self._summary = IPSummary(response)
- return self._summary
-
def _api_get_whois(self):
"""Query the pDNS API for resolution history."""
self._whois = get_api('Whois').get_whois_details(query=self._ip)
@@ -129,50 +114,19 @@
"""History of :class:`passivetotal.analyzer.ssl.Certificates`
presented by services hosted on this IP address.
"""
- if getattr(self, '_ssl_history'):
+ if getattr(self, '_ssl_history', None) is not None:
return self._ssl_history
return self._api_get_ssl_history()
@property
- def resolutions(self):
- """:class:`passivetotal.analyzer.pdns.PdnsResolutions` where this
- IP was the DNS response value.
-
- Bounded by dates set in :meth:`passivetotal.analyzer.set_date_range`.
- `timeout` and `sources` params are also set by the analyzer
configuration.
-
- Provides list of :class:`passivetotal.analyzer.pdns.PdnsRecord`
objects.
- """
- if getattr(self, '_resolutions'):
- return self._resolutions
- config = get_config()
- return self._api_get_resolutions(
- unique=False,
- start_date=config['start_date'],
- end_date=config['end_date'],
- timeout=config['pdns_timeout'],
- sources=config['pdns_sources']
- )
-
- @property
def services(self):
- if getattr(self, '_services'):
+ if getattr(self, '_services', None) is not None:
return self._services
return self._api_get_services()
-
- @property
- def summary(self):
- """Summary of PassiveTotal data available for this IP.
-
- :rtype: :class:`passivetotal.analyzer.summary.IPSummary`
- """
- if getattr(self, '_summary'):
- return self._summary
- return self._api_get_summary()
@property
def whois(self):
"""Whois record details for this IP."""
- if getattr(self, '_whois'):
+ if getattr(self, '_whois', None) is not None:
return self._whois
return self._api_get_whois()
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/pdns.py
new/passivetotal-2.4.0/passivetotal/analyzer/pdns.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/pdns.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/pdns.py 2021-05-10
18:44:25.000000000 +0200
@@ -1,5 +1,5 @@
from datetime import datetime
-from passivetotal.analyzer import get_config
+from passivetotal.analyzer import get_config, get_api
from passivetotal.analyzer._common import RecordList, Record, FirstLastSeen
@@ -162,4 +162,42 @@
@property
def rawrecord(self):
return self._rawrecord
-
\ No newline at end of file
+
+
+
+class HasResolutions:
+ """An object with pDNS resolutions."""
+
+ def _api_get_resolutions(self, unique=False, start_date=None,
end_date=None, timeout=None, sources=None):
+ """Query the pDNS API for resolution history."""
+ meth = get_api('DNS').get_unique_resolutions if unique else
get_api('DNS').get_passive_dns
+ response = meth(
+ query=self.get_host_identifier(),
+ start=start_date,
+ end=end_date,
+ timeout=timeout,
+ sources=sources
+ )
+ self._resolutions = PdnsResolutions(api_response=response)
+ return self._resolutions
+
+ @property
+ def resolutions(self):
+ """:class:`passivetotal.analyzer.pdns.PdnsResolutions` where this
+ object was the DNS response value.
+
+ Bounded by dates set in :meth:`passivetotal.analyzer.set_date_range`.
+ `timeout` and `sources` params are also set by the analyzer
configuration.
+
+ Provides list of :class:`passivetotal.analyzer.pdns.PdnsRecord`
objects.
+ """
+ if getattr(self, '_resolutions', None) is not None:
+ return self._resolutions
+ config = get_config()
+ return self._api_get_resolutions(
+ unique=False,
+ start_date=config['start_date'],
+ end_date=config['end_date'],
+ timeout=config['pdns_timeout'],
+ sources=config['pdns_sources']
+ )
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/services.py
new/passivetotal-2.4.0/passivetotal/analyzer/services.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/services.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/services.py 2021-05-10
18:44:25.000000000 +0200
@@ -1,11 +1,10 @@
from datetime import datetime
-import pprint
-from passivetotal.analyzer._common import RecordList, Record, FirstLastSeen
+from passivetotal.analyzer._common import RecordList, Record, PrettyRecord,
PrettyList, FirstLastSeen
from passivetotal.analyzer.ssl import CertHistoryRecord
-from passivetotal.analyzer import get_api, get_config
+from passivetotal.analyzer import get_api
-class Services(RecordList):
+class Services(RecordList, PrettyList):
"""Historical port, service and banner data."""
@@ -53,7 +52,7 @@
-class ServiceRecord(Record, FirstLastSeen):
+class ServiceRecord(Record, FirstLastSeen, PrettyRecord):
"""Record of an observed port with current and recent services."""
@@ -85,12 +84,6 @@
'lastseen'
]
}
-
- @property
- def pretty(self):
- """Pretty printed version of services data."""
- config = get_config('pprint')
- return pprint.pformat(self.as_dict, **config)
@property
def port(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/ssl.py
new/passivetotal-2.4.0/passivetotal/analyzer/ssl.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/ssl.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/ssl.py 2021-05-10
18:44:25.000000000 +0200
@@ -1,7 +1,7 @@
from datetime import datetime
import pprint
-from passivetotal.analyzer._common import RecordList, Record, FirstLastSeen
-from passivetotal.analyzer import get_api, get_config
+from passivetotal.analyzer._common import RecordList, Record, FirstLastSeen,
AnalyzerError
+from passivetotal.analyzer import get_api, get_config, get_object
@@ -86,7 +86,10 @@
"""Use the 'SSL' request wrapper to perform an SSL certificate search
by field."""
if type(self._value) == list:
raise ValueError('Cannot search a list')
- response =
get_api('SSL').search_ssl_certificate_by_field(query=self._value,
field=self._name)
+ try:
+ response =
get_api('SSL').search_ssl_certificate_by_field(query=self._value,
field=self._name)
+ except Exception:
+ raise AnalyzerError
self._certificates = Certificates(response)
return self._certificates
@@ -155,6 +158,40 @@
self._values[fieldname] = CertificateField(fieldname,
self._cert_details.get(fieldname))
return self._values[fieldname]
+ def _api_get_ip_history(self):
+ try:
+ response =
get_api('SSL').get_ssl_certificate_history(query=self.hash)
+ except Exception as e:
+ raise AnalyzerError
+ self._ip_history = response['results'][0]
+ return self._ip_history
+
+ @property
+ def iphistory(self):
+ """Get the direct API response for a history query on this
certificates hash.
+
+ For most use cases, the `ips` property is a more direct route to get
the list
+ of IPs previously associated with this SSL certificate.
+ """
+ if getattr(self, '_ip_history', None) is not None:
+ return self._ip_history
+ return self._api_get_ip_history()
+
+ @property
+ def ips(self):
+ """Provides list of :class:`passivetotal.analyzer.IPAddress` instances
+ representing IP addresses associated with this SSL certificate."""
+ history = self.iphistory
+ ips = []
+ if history['ipAddresses'] == 'N/A':
+ return ips
+ for ip in history['ipAddresses']:
+ try:
+ ips.append(get_object(ip,'IPAddress'))
+ except AnalyzerError:
+ continue
+ return ips
+
@property
def as_dict(self):
"""All SSL fields as a mapping with string values."""
@@ -480,16 +517,21 @@
self._ips = record.get('ipAddresses',[])
def __str__(self):
- ips = 'ip' if len(self._ips)==1 else 'ips'
- return '{0.hash} on {ipcount} {ips} from {0.firstseen_date} to
{0.lastseen_date}'.format(self, ipcount=len(self._ips), ips=ips)
+ return '{0.hash} from {0.firstseen_date} to
{0.lastseen_date}'.format(self)
def __repr__(self):
return "<CertHistoryRecord '{0.hash}'>".format(self)
def _api_get_details(self):
"""Query the SSL API for certificate details."""
- response = get_api('SSL').get_ssl_certificate_details(query=self._sha1)
- self._cert_details = response['results'][0] # API oddly returns an
array
+ try:
+ response =
get_api('SSL').get_ssl_certificate_details(query=self._sha1)
+ except Exception:
+ raise AnalyzerError
+ try:
+ self._cert_details = response['results'][0] # API oddly returns an
array
+ except IndexError:
+ raise SSLAnalyzerError('No details available for this certificate')
self._has_details = True
return self._cert_details
@@ -502,13 +544,10 @@
return
self._api_get_details()
- @property
- def ips(self):
- """Provides list of :class:`passivetotal.analyzer.IPAddress` instances
- representing IP addresses associated with this SSL certificate."""
- from passivetotal.analyzer import IPAddress
- for ip in self._ips:
- yield IPAddress(ip)
+class SSLAnalyzerError(AnalyzerError):
+ """An exception raised when accessing SSL properties in the Analyzer
module."""
+ pass
+
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/summary.py
new/passivetotal-2.4.0/passivetotal/analyzer/summary.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/summary.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/summary.py 2021-05-10
18:44:25.000000000 +0200
@@ -1,4 +1,4 @@
-from passivetotal.analyzer import get_config
+
@@ -170,4 +170,18 @@
def services(self):
"""Number of service (port) history records for this IP."""
return self._count_or_none('services')
-
\ No newline at end of file
+
+
+
+class HasSummary:
+ """An object with summary card data."""
+
+ @property
+ def summary(self):
+ """Summary of PassiveTotal data available for this hostname.
+
+ :rtype: :class:`passivetotal.analyzer.summary.HostnameSummary`
+ """
+ if getattr(self, '_summary', None) is not None:
+ return self._summary
+ return self._api_get_summary()
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/analyzer/trackers.py
new/passivetotal-2.4.0/passivetotal/analyzer/trackers.py
--- old/passivetotal-2.3.0/passivetotal/analyzer/trackers.py 2021-04-13
02:19:09.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal/analyzer/trackers.py 2021-05-10
18:44:25.000000000 +0200
@@ -3,7 +3,7 @@
from passivetotal.analyzer._common import (
RecordList, Record, FirstLastSeen, PagedRecordList
)
-from passivetotal.analyzer import get_api, get_config
+from passivetotal.analyzer import get_api, get_config, get_object
@@ -27,9 +27,8 @@
@property
def hostnames(self):
"""List of unique hostnames in the tracker record list."""
- from passivetotal.analyzer import Hostname
return set(
- Hostname(host) for host in set([record.hostname for record in
self])
+ get_object(host) for host in set([record.hostname for record in
self])
)
@property
@@ -122,7 +121,7 @@
:rtype: :class:`passivetotal.analyzer.trackers.TrackersHistory`
"""
- if getattr(self, '_trackers'):
+ if getattr(self, '_trackers', None) is not None:
return self._trackers
config = get_config()
return self._api_get_trackers(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal/libs/articles.py
new/passivetotal-2.4.0/passivetotal/libs/articles.py
--- old/passivetotal-2.3.0/passivetotal/libs/articles.py 2021-03-15
15:30:16.000000000 +0100
+++ new/passivetotal-2.4.0/passivetotal/libs/articles.py 2021-05-10
18:44:25.000000000 +0200
@@ -45,6 +45,17 @@
:return: Dict of results
"""
return self._get('articles', 'indicators', **kwargs)
+
+ def get_articles_for_indicator(self, indicator, indicator_type=None):
+ """Get articles that reference an indicator (typically a domain or IP).
+
+ Reference:
https://api.riskiq.net/api/articles/#!/default/get_pt_v2_articles_indicator
+
+ :param indicator: Indicator to search, typically domain or IP
+ :param indicator_type: Type of indicator to search for (optional)
+ :return: Dict of results
+ """
+ return self._get('articles', 'indicator', query=indicator,
type=indicator_type)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal.egg-info/PKG-INFO
new/passivetotal-2.4.0/passivetotal.egg-info/PKG-INFO
--- old/passivetotal-2.3.0/passivetotal.egg-info/PKG-INFO 2021-04-14
23:18:17.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal.egg-info/PKG-INFO 2021-05-10
18:45:03.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: passivetotal
-Version: 2.3.0
+Version: 2.4.0
Summary: Library for the RiskIQ PassiveTotal and Illuminate API
Home-page: https://github.com/passivetotal/python_api
Author: RiskIQ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/passivetotal.egg-info/SOURCES.txt
new/passivetotal-2.4.0/passivetotal.egg-info/SOURCES.txt
--- old/passivetotal-2.3.0/passivetotal.egg-info/SOURCES.txt 2021-04-14
23:18:17.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal.egg-info/SOURCES.txt 2021-05-10
18:45:03.000000000 +0200
@@ -16,8 +16,10 @@
passivetotal.egg-info/top_level.txt
passivetotal/analyzer/__init__.py
passivetotal/analyzer/_common.py
+passivetotal/analyzer/articles.py
passivetotal/analyzer/components.py
passivetotal/analyzer/cookies.py
+passivetotal/analyzer/enrich.py
passivetotal/analyzer/hostname.py
passivetotal/analyzer/hostpairs.py
passivetotal/analyzer/illuminate.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/passivetotal-2.3.0/passivetotal.egg-info/requires.txt
new/passivetotal-2.4.0/passivetotal.egg-info/requires.txt
--- old/passivetotal-2.3.0/passivetotal.egg-info/requires.txt 2021-04-14
23:18:17.000000000 +0200
+++ new/passivetotal-2.4.0/passivetotal.egg-info/requires.txt 2021-05-10
18:45:03.000000000 +0200
@@ -2,3 +2,4 @@
ez_setup
python-dateutil
future
+tldextract
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/passivetotal-2.3.0/setup.py
new/passivetotal-2.4.0/setup.py
--- old/passivetotal-2.3.0/setup.py 2021-04-14 23:16:56.000000000 +0200
+++ new/passivetotal-2.4.0/setup.py 2021-05-10 18:44:25.000000000 +0200
@@ -8,14 +8,14 @@
setup(
name='passivetotal',
- version='2.3.0',
+ version='2.4.0',
description='Library for the RiskIQ PassiveTotal and Illuminate API',
url="https://github.com/passivetotal/python_api",
author="RiskIQ",
author_email="[email protected]",
license="GPLv2",
packages=find_packages(),
- install_requires=['requests', 'ez_setup', 'python-dateutil', 'future'],
+ install_requires=['requests', 'ez_setup', 'python-dateutil', 'future',
'tldextract'],
long_description=read('README.md'),
long_description_content_type="text/markdown",
classifiers=[],