polyswarm-api 4.2.0__tar.gz → 4.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polyswarm_api-4.2.0/src/polyswarm_api.egg-info → polyswarm_api-4.4.0}/PKG-INFO +1 -1
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/pyproject.toml +14 -9
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/__init__.py +1 -1
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/api.py +106 -8
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/api.py +117 -7
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/core.py +43 -3
- polyswarm_api-4.4.0/src/polyswarm_api/exceptions.py +135 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/resources.py +57 -3
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0/src/polyswarm_api.egg-info}/PKG-INFO +1 -1
- polyswarm_api-4.2.0/src/polyswarm_api/exceptions.py +0 -85
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/LICENSE +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/README.md +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/setup.cfg +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/__init__.py +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/session.py +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/session.py +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api/settings.py +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/SOURCES.txt +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/dependency_links.txt +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/requires.txt +0 -0
- {polyswarm_api-4.2.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "polyswarm_api"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.4.0"
|
|
8
8
|
description = "Client library to simplify interacting with the PolySwarm consumer API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<4"
|
|
@@ -55,7 +55,7 @@ package-dir = { "" = "src" }
|
|
|
55
55
|
where = ["src"]
|
|
56
56
|
|
|
57
57
|
[tool.bumpversion]
|
|
58
|
-
current_version = "4.
|
|
58
|
+
current_version = "4.4.0"
|
|
59
59
|
commit = true
|
|
60
60
|
tag = false
|
|
61
61
|
sign_tags = true
|
|
@@ -76,11 +76,17 @@ search = "__version__ = '{current_version}'"
|
|
|
76
76
|
replace = "__version__ = '{new_version}'"
|
|
77
77
|
|
|
78
78
|
[tool.pytest.ini_options]
|
|
79
|
-
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
79
|
+
# Live logging OFF by default. When on, pytest streams log records as the run goes
|
|
80
|
+
# AND forces every test onto its own nodeid line (verbose-style) — ~130 lines of
|
|
81
|
+
# pure noise on a green xdist run. With it off the run prints dots + an end
|
|
82
|
+
# summary, and a FAILING test still shows its captured logs (at log_level, below)
|
|
83
|
+
# plus the traceback. Opt back into live streaming with TESTS_LOG_CLI=1 (or an
|
|
84
|
+
# explicit --log-cli-level) when debugging a live run; see test/conftest.py.
|
|
85
|
+
log_cli = false
|
|
86
|
+
# Level used for live logging when it's opted into (TESTS_LOG_CLI=1). INFO keeps
|
|
87
|
+
# the useful app request/response lines while dropping the DEBUG firehose (VCR
|
|
88
|
+
# per-request match trace, parse traces) that otherwise bloats logs ~1000x.
|
|
89
|
+
# TESTS_LOG_LEVEL overrides both this and the captured-log level (conftest.py).
|
|
84
90
|
log_cli_level = "INFO"
|
|
85
91
|
log_format = "%(asctime)s %(levelname)-2s [%(name)s:%(filename)s:%(lineno)d:%(funcName)1s()] %(message)s"
|
|
86
92
|
python_files = ["test_*.py", "*_test.py"]
|
|
@@ -95,7 +101,6 @@ filterwarnings = [
|
|
|
95
101
|
asyncio_mode = "auto"
|
|
96
102
|
addopts = [
|
|
97
103
|
"--continue-on-collection-errors",
|
|
98
|
-
"-v",
|
|
99
104
|
"-s",
|
|
100
|
-
"-
|
|
105
|
+
"-ra",
|
|
101
106
|
]
|
|
@@ -18,7 +18,7 @@ import logging
|
|
|
18
18
|
import time
|
|
19
19
|
|
|
20
20
|
from polyswarm_api import exceptions, resources, settings
|
|
21
|
-
from polyswarm_api.core import PolyswarmRequest
|
|
21
|
+
from polyswarm_api.core import PolyswarmRequest, _as_result_bound
|
|
22
22
|
|
|
23
23
|
from .session import AsyncPolyswarmSession
|
|
24
24
|
|
|
@@ -502,23 +502,35 @@ class PolySwarmAsyncAPI:
|
|
|
502
502
|
return await self._single(resources.LiveYaraRuleset.delete(self, rule_id=rule_id))
|
|
503
503
|
|
|
504
504
|
async def live_feed(self, since=None, rule_name=None, family=None,
|
|
505
|
-
polyscore_lower=None, polyscore_upper=None, community=None
|
|
505
|
+
polyscore_lower=None, polyscore_upper=None, community=None,
|
|
506
|
+
livescan_id=None, max_results=None):
|
|
506
507
|
"""
|
|
507
508
|
Get live hunts feed
|
|
508
509
|
|
|
509
|
-
:param since:
|
|
510
|
+
:param since: Window in SECONDS (this said "minutes" in earlier releases and was
|
|
511
|
+
wrong). Absent or 0 means no time filter at all.
|
|
510
512
|
:param rule_name: Filter hunt results on the provided rule name (exact match).
|
|
511
513
|
:param family: Filter hunt results based on the family name (exact match).
|
|
512
514
|
:param polyscore_lower: Polyscore lower bound for the hunt results.
|
|
513
515
|
:param polyscore_upper: Polyscore upper bound for the hunt results.
|
|
514
516
|
:param community: Community to retrieve live results from, or public/private.
|
|
517
|
+
:param livescan_id: Scope the feed to one live hunt's results.
|
|
518
|
+
:param max_results: Total results to yield, not a page size — paging
|
|
519
|
+
continues in the server's own chunks until the total is reached.
|
|
520
|
+
None, 0 or a negative means no bound: every page, as before.
|
|
515
521
|
:return: Generator of HuntResult resources
|
|
516
522
|
"""
|
|
523
|
+
bound = _as_result_bound(max_results)
|
|
524
|
+
yielded = 0
|
|
517
525
|
async for item in self._paginate(resources.LiveHuntResult.list(
|
|
518
526
|
self, since=since, rule_name=rule_name, family=family,
|
|
519
527
|
polyscore_lower=polyscore_lower, polyscore_upper=polyscore_upper,
|
|
528
|
+
livescan_id=livescan_id,
|
|
520
529
|
community=community or self.community)):
|
|
521
530
|
yield item
|
|
531
|
+
yielded += 1
|
|
532
|
+
if bound is not None and yielded >= bound:
|
|
533
|
+
return
|
|
522
534
|
|
|
523
535
|
async def live_feed_delete(self, result_ids):
|
|
524
536
|
"""
|
|
@@ -684,15 +696,45 @@ class PolySwarmAsyncAPI:
|
|
|
684
696
|
logger.info('Delete ruleset %s', ruleset_id)
|
|
685
697
|
return await self._single(resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community))
|
|
686
698
|
|
|
687
|
-
async def ruleset_list(self
|
|
699
|
+
async def ruleset_list(self, name=None, status=None, favorites_only=None,
|
|
700
|
+
has_new_results=None):
|
|
688
701
|
"""
|
|
689
702
|
List all YaraRulesets for the current account.
|
|
703
|
+
|
|
704
|
+
All filters are optional and conjunctive:
|
|
705
|
+
:param name: Case-insensitive substring match on the ruleset name.
|
|
706
|
+
:param status: 'active' returns only rulesets whose live hunt is
|
|
707
|
+
currently running.
|
|
708
|
+
:param favorites_only: True returns only favorited rulesets.
|
|
709
|
+
:param has_new_results: True returns only rulesets whose stored
|
|
710
|
+
new-results counter is positive. The counter (and its window) is
|
|
711
|
+
maintained server-side by a scheduled refresh; rows carry it as
|
|
712
|
+
``new_results_count`` with ``new_results_counted_at`` marking when
|
|
713
|
+
it was last refreshed. There is no per-request window parameter.
|
|
690
714
|
:return: A generator of YaraRuleset resources
|
|
691
715
|
"""
|
|
692
716
|
logger.info('List rulesets')
|
|
693
|
-
async for item in self._paginate(resources.YaraRuleset.list(
|
|
717
|
+
async for item in self._paginate(resources.YaraRuleset.list(
|
|
718
|
+
self, name=name, status=status, favorites_only=favorites_only,
|
|
719
|
+
has_new_results=has_new_results, community=self.community)):
|
|
694
720
|
yield item
|
|
695
721
|
|
|
722
|
+
async def ruleset_favorite(self, ruleset_id, favorite=True):
|
|
723
|
+
"""
|
|
724
|
+
Favorite or unfavorite a YaraRuleset. Idempotent; works while a live
|
|
725
|
+
hunt is running. Favorites are shared by the whole team and capped
|
|
726
|
+
(the response carries favorites_used / favorites_limit); when the
|
|
727
|
+
budget is exhausted the server refuses with a machine-readable
|
|
728
|
+
FAVORITE_LIMIT error.
|
|
729
|
+
|
|
730
|
+
:param ruleset_id: Id of the ruleset
|
|
731
|
+
:param favorite: True to star, False to unstar
|
|
732
|
+
:return: A YaraRulesetFavorite resource
|
|
733
|
+
"""
|
|
734
|
+
logger.info('%s ruleset %s', 'Favorite' if favorite else 'Unfavorite', ruleset_id)
|
|
735
|
+
return await self._single(resources.YaraRulesetFavorite.update(
|
|
736
|
+
self, id=ruleset_id, favorite=favorite, community=self.community))
|
|
737
|
+
|
|
696
738
|
async def tag_link_get(self, sha256):
|
|
697
739
|
"""
|
|
698
740
|
Fetch the Tags and Families associated with the given sha256.
|
|
@@ -939,6 +981,13 @@ class PolySwarmAsyncAPI:
|
|
|
939
981
|
:param fh: A file-like object which we are going to write the contents of the artifact to.
|
|
940
982
|
:param hash_type: Hash type of the provided hash_. Will attempt to auto-detect if not explicitly provided.
|
|
941
983
|
:return: A LocalHandle resource
|
|
984
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
985
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
986
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
987
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
988
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
989
|
+
the status is checked before any write (the handle is the caller's and arrives
|
|
990
|
+
already open, so "before the file is opened" would be the wrong reason here).
|
|
942
991
|
"""
|
|
943
992
|
logger.info('Downloading %s into handle', hash_)
|
|
944
993
|
hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)
|
|
@@ -1599,6 +1648,12 @@ class PolySwarmAsyncAPI:
|
|
|
1599
1648
|
:param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5.
|
|
1600
1649
|
:param hash_type: Hash type; auto-detected from the literal if not provided.
|
|
1601
1650
|
:return: A ``LocalArtifact`` resource with its handle already closed.
|
|
1651
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
1652
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
1653
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
1654
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
1655
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
1656
|
+
the status is checked before the file is opened.
|
|
1602
1657
|
"""
|
|
1603
1658
|
logger.info('Downloading %s into %s', hash_, out_dir)
|
|
1604
1659
|
hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)
|
|
@@ -1609,7 +1664,15 @@ class PolySwarmAsyncAPI:
|
|
|
1609
1664
|
return artifact
|
|
1610
1665
|
|
|
1611
1666
|
async def download_id(self, out_dir, instance_id):
|
|
1612
|
-
"""Download an artifact by its instance id into ``out_dir``.
|
|
1667
|
+
"""Download an artifact by its instance id into ``out_dir``.
|
|
1668
|
+
|
|
1669
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
1670
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
1671
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
1672
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
1673
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
1674
|
+
the status is checked before the file is opened.
|
|
1675
|
+
"""
|
|
1613
1676
|
logger.info('Downloading %s into %s', instance_id, out_dir)
|
|
1614
1677
|
artifact = await self._single(
|
|
1615
1678
|
resources.LocalArtifact.download_id(self, instance_id, folder=out_dir),
|
|
@@ -1618,7 +1681,21 @@ class PolySwarmAsyncAPI:
|
|
|
1618
1681
|
return artifact
|
|
1619
1682
|
|
|
1620
1683
|
async def download_sandbox_artifact(self, out_dir, sandbox_task_id, instance_id):
|
|
1621
|
-
"""Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``.
|
|
1684
|
+
"""Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``.
|
|
1685
|
+
|
|
1686
|
+
The known-good gate applies to **every** sandbox artifact by its own sha256 —
|
|
1687
|
+
dropped file, screenshot, report, … — with no sample-vs-evidence carve-out
|
|
1688
|
+
server-side, so any member whose digest is catalogued (and eligible) raises below.
|
|
1689
|
+
In practice that is almost always a dropped file; evidence collides only on byte
|
|
1690
|
+
identity, and the empty-file digest is refused at the catalogue boundary instead.
|
|
1691
|
+
|
|
1692
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
1693
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
1694
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
1695
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
1696
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
1697
|
+
the status is checked before the file is opened.
|
|
1698
|
+
"""
|
|
1622
1699
|
logger.info('Downloading sandbox artifact %s %s', sandbox_task_id, instance_id)
|
|
1623
1700
|
sandbox_artifact = await self._single(
|
|
1624
1701
|
resources.LocalArtifact.download_sandbox_artifact(
|
|
@@ -1642,8 +1719,29 @@ class PolySwarmAsyncAPI:
|
|
|
1642
1719
|
|
|
1643
1720
|
:param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5.
|
|
1644
1721
|
:param hash_type: Hash type; auto-detected if not provided.
|
|
1645
|
-
:param require_scan: If True, only count artifacts that have been scanned
|
|
1722
|
+
:param require_scan: If True, only count artifacts that have been *scanned*.
|
|
1723
|
+
Being catalogued as known-good is **not** a scan: the platform never scans
|
|
1724
|
+
such a sample, so a hash whose only record is a known-good reference reports
|
|
1725
|
+
absent under ``require_scan`` (and present without it, because that reference
|
|
1726
|
+
is a real searchable record).
|
|
1646
1727
|
:return: ``True`` if the artifact exists in PolySwarm's index.
|
|
1728
|
+
|
|
1729
|
+
.. note::
|
|
1730
|
+
This is an existence probe, and its status codes are a **frozen contract**:
|
|
1731
|
+
``200`` means found, ``204`` means not found, and anything else means the
|
|
1732
|
+
*request* was wrong.
|
|
1733
|
+
|
|
1734
|
+
Only ``200`` is ``True``. ``204`` and ``404`` are both ``False`` — the server
|
|
1735
|
+
answers ``204`` for absent, and ``404`` is tolerated as absent for historical
|
|
1736
|
+
reasons.
|
|
1737
|
+
|
|
1738
|
+
A non-2xx status does not raise here, and the reason is the **method**, not a
|
|
1739
|
+
missing result parser: ``parse_response`` short-circuits ``HEAD``, setting the
|
|
1740
|
+
status code as the result and returning before it reaches the non-2xx→exception
|
|
1741
|
+
mapping (which otherwise applies whether or not a parser is set). So on this
|
|
1742
|
+
endpoint a server error also collapses to ``False``, indistinguishable from a
|
|
1743
|
+
genuine "does not exist" — which is why neither side may widen what counts as
|
|
1744
|
+
found. See ``specs/03-endpoints.md``.
|
|
1647
1745
|
"""
|
|
1648
1746
|
logger.info('Exists for hash %s', hash_)
|
|
1649
1747
|
hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)
|
|
@@ -20,7 +20,7 @@ import io
|
|
|
20
20
|
import logging
|
|
21
21
|
|
|
22
22
|
from polyswarm_api import exceptions, resources, settings
|
|
23
|
-
from polyswarm_api.core import PolyswarmRequest
|
|
23
|
+
from polyswarm_api.core import PolyswarmRequest, _as_result_bound
|
|
24
24
|
|
|
25
25
|
from .session import PolyswarmSession
|
|
26
26
|
|
|
@@ -568,18 +568,27 @@ class PolyswarmAPI:
|
|
|
568
568
|
polyscore_lower=None,
|
|
569
569
|
polyscore_upper=None,
|
|
570
570
|
community=None,
|
|
571
|
+
livescan_id=None,
|
|
572
|
+
max_results=None,
|
|
571
573
|
):
|
|
572
574
|
"""
|
|
573
575
|
Get live hunts feed
|
|
574
576
|
|
|
575
|
-
:param since:
|
|
577
|
+
:param since: Window in SECONDS (this said "minutes" in earlier releases and was
|
|
578
|
+
wrong). Absent or 0 means no time filter at all.
|
|
576
579
|
:param rule_name: Filter hunt results on the provided rule name (exact match).
|
|
577
580
|
:param family: Filter hunt results based on the family name (exact match).
|
|
578
581
|
:param polyscore_lower: Polyscore lower bound for the hunt results.
|
|
579
582
|
:param polyscore_upper: Polyscore upper bound for the hunt results.
|
|
580
583
|
:param community: Community to retrieve live results from, or public/private.
|
|
584
|
+
:param livescan_id: Scope the feed to one live hunt's results.
|
|
585
|
+
:param max_results: Total results to yield, not a page size — paging
|
|
586
|
+
continues in the server's own chunks until the total is reached.
|
|
587
|
+
None, 0 or a negative means no bound: every page, as before.
|
|
581
588
|
:return: Generator of HuntResult resources
|
|
582
589
|
"""
|
|
590
|
+
bound = _as_result_bound(max_results)
|
|
591
|
+
yielded = 0
|
|
583
592
|
for item in self._paginate(
|
|
584
593
|
resources.LiveHuntResult.list(
|
|
585
594
|
self,
|
|
@@ -588,10 +597,14 @@ class PolyswarmAPI:
|
|
|
588
597
|
family=family,
|
|
589
598
|
polyscore_lower=polyscore_lower,
|
|
590
599
|
polyscore_upper=polyscore_upper,
|
|
600
|
+
livescan_id=livescan_id,
|
|
591
601
|
community=community or self.community,
|
|
592
602
|
)
|
|
593
603
|
):
|
|
594
604
|
yield item
|
|
605
|
+
yielded += 1
|
|
606
|
+
if bound is not None and yielded >= bound:
|
|
607
|
+
return
|
|
595
608
|
|
|
596
609
|
def live_feed_delete(self, result_ids):
|
|
597
610
|
"""
|
|
@@ -823,17 +836,58 @@ class PolyswarmAPI:
|
|
|
823
836
|
resources.YaraRuleset.delete(self, id=ruleset_id, community=self.community)
|
|
824
837
|
)
|
|
825
838
|
|
|
826
|
-
def ruleset_list(
|
|
839
|
+
def ruleset_list(
|
|
840
|
+
self, name=None, status=None, favorites_only=None, has_new_results=None
|
|
841
|
+
):
|
|
827
842
|
"""
|
|
828
843
|
List all YaraRulesets for the current account.
|
|
844
|
+
|
|
845
|
+
All filters are optional and conjunctive:
|
|
846
|
+
:param name: Case-insensitive substring match on the ruleset name.
|
|
847
|
+
:param status: 'active' returns only rulesets whose live hunt is
|
|
848
|
+
currently running.
|
|
849
|
+
:param favorites_only: True returns only favorited rulesets.
|
|
850
|
+
:param has_new_results: True returns only rulesets whose stored
|
|
851
|
+
new-results counter is positive. The counter (and its window) is
|
|
852
|
+
maintained server-side by a scheduled refresh; rows carry it as
|
|
853
|
+
``new_results_count`` with ``new_results_counted_at`` marking when
|
|
854
|
+
it was last refreshed. There is no per-request window parameter.
|
|
829
855
|
:return: A generator of YaraRuleset resources
|
|
830
856
|
"""
|
|
831
857
|
logger.info("List rulesets")
|
|
832
858
|
for item in self._paginate(
|
|
833
|
-
resources.YaraRuleset.list(
|
|
859
|
+
resources.YaraRuleset.list(
|
|
860
|
+
self,
|
|
861
|
+
name=name,
|
|
862
|
+
status=status,
|
|
863
|
+
favorites_only=favorites_only,
|
|
864
|
+
has_new_results=has_new_results,
|
|
865
|
+
community=self.community,
|
|
866
|
+
)
|
|
834
867
|
):
|
|
835
868
|
yield item
|
|
836
869
|
|
|
870
|
+
def ruleset_favorite(self, ruleset_id, favorite=True):
|
|
871
|
+
"""
|
|
872
|
+
Favorite or unfavorite a YaraRuleset. Idempotent; works while a live
|
|
873
|
+
hunt is running. Favorites are shared by the whole team and capped
|
|
874
|
+
(the response carries favorites_used / favorites_limit); when the
|
|
875
|
+
budget is exhausted the server refuses with a machine-readable
|
|
876
|
+
FAVORITE_LIMIT error.
|
|
877
|
+
|
|
878
|
+
:param ruleset_id: Id of the ruleset
|
|
879
|
+
:param favorite: True to star, False to unstar
|
|
880
|
+
:return: A YaraRulesetFavorite resource
|
|
881
|
+
"""
|
|
882
|
+
logger.info(
|
|
883
|
+
"%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id
|
|
884
|
+
)
|
|
885
|
+
return self._single(
|
|
886
|
+
resources.YaraRulesetFavorite.update(
|
|
887
|
+
self, id=ruleset_id, favorite=favorite, community=self.community
|
|
888
|
+
)
|
|
889
|
+
)
|
|
890
|
+
|
|
837
891
|
def tag_link_get(self, sha256):
|
|
838
892
|
"""
|
|
839
893
|
Fetch the Tags and Families associated with the given sha256.
|
|
@@ -1139,6 +1193,13 @@ class PolyswarmAPI:
|
|
|
1139
1193
|
:param fh: A file-like object which we are going to write the contents of the artifact to.
|
|
1140
1194
|
:param hash_type: Hash type of the provided hash_. Will attempt to auto-detect if not explicitly provided.
|
|
1141
1195
|
:return: A LocalHandle resource
|
|
1196
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
1197
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
1198
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
1199
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
1200
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
1201
|
+
the status is checked before any write (the handle is the caller's and arrives
|
|
1202
|
+
already open, so "before the file is opened" would be the wrong reason here).
|
|
1142
1203
|
"""
|
|
1143
1204
|
logger.info("Downloading %s into handle", hash_)
|
|
1144
1205
|
hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)
|
|
@@ -1941,6 +2002,12 @@ class PolyswarmAPI:
|
|
|
1941
2002
|
:param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5.
|
|
1942
2003
|
:param hash_type: Hash type; auto-detected from the literal if not provided.
|
|
1943
2004
|
:return: A ``LocalArtifact`` resource with its handle already closed.
|
|
2005
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
2006
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
2007
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
2008
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
2009
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
2010
|
+
the status is checked before the file is opened.
|
|
1944
2011
|
"""
|
|
1945
2012
|
logger.info("Downloading %s into %s", hash_, out_dir)
|
|
1946
2013
|
hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)
|
|
@@ -1953,7 +2020,15 @@ class PolyswarmAPI:
|
|
|
1953
2020
|
return artifact
|
|
1954
2021
|
|
|
1955
2022
|
def download_id(self, out_dir, instance_id):
|
|
1956
|
-
"""Download an artifact by its instance id into ``out_dir``.
|
|
2023
|
+
"""Download an artifact by its instance id into ``out_dir``.
|
|
2024
|
+
|
|
2025
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
2026
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
2027
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
2028
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
2029
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
2030
|
+
the status is checked before the file is opened.
|
|
2031
|
+
"""
|
|
1957
2032
|
logger.info("Downloading %s into %s", instance_id, out_dir)
|
|
1958
2033
|
artifact = self._single(
|
|
1959
2034
|
resources.LocalArtifact.download_id(self, instance_id, folder=out_dir),
|
|
@@ -1962,7 +2037,21 @@ class PolyswarmAPI:
|
|
|
1962
2037
|
return artifact
|
|
1963
2038
|
|
|
1964
2039
|
def download_sandbox_artifact(self, out_dir, sandbox_task_id, instance_id):
|
|
1965
|
-
"""Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``.
|
|
2040
|
+
"""Download a sandbox-produced artifact (e.g. PCAP, dropped file) into ``out_dir``.
|
|
2041
|
+
|
|
2042
|
+
The known-good gate applies to **every** sandbox artifact by its own sha256 —
|
|
2043
|
+
dropped file, screenshot, report, … — with no sample-vs-evidence carve-out
|
|
2044
|
+
server-side, so any member whose digest is catalogued (and eligible) raises below.
|
|
2045
|
+
In practice that is almost always a dropped file; evidence collides only on byte
|
|
2046
|
+
identity, and the empty-file digest is refused at the catalogue boundary instead.
|
|
2047
|
+
|
|
2048
|
+
:raises KnownGoodWithheldException: the sha256 is catalogued as a known-good binary,
|
|
2049
|
+
so its bytes are withheld by design. A ``NotFoundException`` subclass, so existing
|
|
2050
|
+
``except NotFoundException`` handling still catches it; catch it specifically to
|
|
2051
|
+
tell a deliberate refusal apart from a missing artifact, and read
|
|
2052
|
+
``.sources`` for the feeds that flagged it. Nothing is written to the destination —
|
|
2053
|
+
the status is checked before the file is opened.
|
|
2054
|
+
"""
|
|
1966
2055
|
logger.info("Downloading sandbox artifact %s %s", sandbox_task_id, instance_id)
|
|
1967
2056
|
sandbox_artifact = self._single(
|
|
1968
2057
|
resources.LocalArtifact.download_sandbox_artifact(
|
|
@@ -1989,8 +2078,29 @@ class PolyswarmAPI:
|
|
|
1989
2078
|
|
|
1990
2079
|
:param hash_: Hashable (Artifact, LocalArtifact, Hash) or hex-encoded SHA256/SHA1/MD5.
|
|
1991
2080
|
:param hash_type: Hash type; auto-detected if not provided.
|
|
1992
|
-
:param require_scan: If True, only count artifacts that have been scanned
|
|
2081
|
+
:param require_scan: If True, only count artifacts that have been *scanned*.
|
|
2082
|
+
Being catalogued as known-good is **not** a scan: the platform never scans
|
|
2083
|
+
such a sample, so a hash whose only record is a known-good reference reports
|
|
2084
|
+
absent under ``require_scan`` (and present without it, because that reference
|
|
2085
|
+
is a real searchable record).
|
|
1993
2086
|
:return: ``True`` if the artifact exists in PolySwarm's index.
|
|
2087
|
+
|
|
2088
|
+
.. note::
|
|
2089
|
+
This is an existence probe, and its status codes are a **frozen contract**:
|
|
2090
|
+
``200`` means found, ``204`` means not found, and anything else means the
|
|
2091
|
+
*request* was wrong.
|
|
2092
|
+
|
|
2093
|
+
Only ``200`` is ``True``. ``204`` and ``404`` are both ``False`` — the server
|
|
2094
|
+
answers ``204`` for absent, and ``404`` is tolerated as absent for historical
|
|
2095
|
+
reasons.
|
|
2096
|
+
|
|
2097
|
+
A non-2xx status does not raise here, and the reason is the **method**, not a
|
|
2098
|
+
missing result parser: ``parse_response`` short-circuits ``HEAD``, setting the
|
|
2099
|
+
status code as the result and returning before it reaches the non-2xx→exception
|
|
2100
|
+
mapping (which otherwise applies whether or not a parser is set). So on this
|
|
2101
|
+
endpoint a server error also collapses to ``False``, indistinguishable from a
|
|
2102
|
+
genuine "does not exist" — which is why neither side may widen what counts as
|
|
2103
|
+
found. See ``specs/03-endpoints.md``.
|
|
1994
2104
|
"""
|
|
1995
2105
|
logger.info("Exists for hash %s", hash_)
|
|
1996
2106
|
hash_ = resources.Hash.from_hashable(hash_, hash_type=hash_type)
|
|
@@ -16,7 +16,8 @@ parsing logic have a single source of truth:
|
|
|
16
16
|
builders (``create``, ``get``, ``head``, ``update``, ``delete``,
|
|
17
17
|
``list``) construct ``PolyswarmRequest`` descriptors.
|
|
18
18
|
- ``Hashable`` + ``is_valid_*`` validators, ``parse_isoformat``,
|
|
19
|
-
``_normalise_bool_params``, ``RequestParamsEncoder
|
|
19
|
+
``_normalise_bool_params``, ``RequestParamsEncoder``,
|
|
20
|
+
``_as_result_bound``.
|
|
20
21
|
|
|
21
22
|
Both transports import from here.
|
|
22
23
|
"""
|
|
@@ -330,7 +331,9 @@ def _raise_for_status(response, request):
|
|
|
330
331
|
|
|
331
332
|
Shared by ``parse_response`` (buffered/JSON path) and the session's
|
|
332
333
|
streaming-download path, so both raise identically (429/404/422/other,
|
|
333
|
-
plus the non-JSON-body fallbacks).
|
|
334
|
+
plus the non-JSON-body fallbacks). A 404 whose ``errors`` payload carries the
|
|
335
|
+
``KNOWN_GOOD`` code raises the ``NotFoundException`` subclass
|
|
336
|
+
``KnownGoodWithheldException``. The response body must already be
|
|
334
337
|
readable — streaming callers ``read()`` / ``aread()`` it first. **Always
|
|
335
338
|
raises; never returns.**
|
|
336
339
|
"""
|
|
@@ -354,6 +357,17 @@ def _raise_for_status(response, request):
|
|
|
354
357
|
)
|
|
355
358
|
raise exceptions.UsageLimitsExceededException(request, message)
|
|
356
359
|
elif request.status_code == 404:
|
|
360
|
+
# A download refused because the artifact is a known-good binary carries a
|
|
361
|
+
# machine-readable code in the error envelope's ``errors`` slot. Raise the
|
|
362
|
+
# ``NotFoundException`` subclass so callers can tell "withheld by design"
|
|
363
|
+
# apart from a plain miss; every other 404 stays a plain NotFoundException.
|
|
364
|
+
# The raw ``sources`` payload goes in as-is — the exception normalises it
|
|
365
|
+
# into the documented list-of-feed-names shape.
|
|
366
|
+
errors = request.errors
|
|
367
|
+
if isinstance(errors, dict) and errors.get('code') == 'KNOWN_GOOD':
|
|
368
|
+
raise exceptions.KnownGoodWithheldException(
|
|
369
|
+
request, request._result, sources=errors.get('sources'),
|
|
370
|
+
)
|
|
357
371
|
raise exceptions.NotFoundException(request, request._result)
|
|
358
372
|
elif request.status_code == 422:
|
|
359
373
|
raise exceptions.FailedInstanceException(request, request._result)
|
|
@@ -383,7 +397,23 @@ def _bad_status_message(request):
|
|
|
383
397
|
f'Message: {request._result}'
|
|
384
398
|
)
|
|
385
399
|
if request.errors:
|
|
386
|
-
errors
|
|
400
|
+
# Two ``errors`` shapes are in play. The legacy shape is a **list** of
|
|
401
|
+
# per-error entries — one rendered line each. The way-forward shape is a
|
|
402
|
+
# **mapping** carrying the machine-readable code plus its context
|
|
403
|
+
# (``{'code': 'KNOWN_GOOD', 'known_good': True, 'sources': [...]}``), and the
|
|
404
|
+
# server forwards it on every status, not just the 404 the code was
|
|
405
|
+
# introduced for. Iterating a mapping yields only its KEYS, so rendering it
|
|
406
|
+
# like a list would drop every value from the message the caller sees —
|
|
407
|
+
# render it as ``key=value`` lines instead.
|
|
408
|
+
# The same reasoning applies to a bare string: iterating it yields characters, so
|
|
409
|
+
# `"errors": "some prose"` would render one letter per line. Only genuinely
|
|
410
|
+
# sequence-shaped payloads get the line-per-entry treatment.
|
|
411
|
+
if isinstance(request.errors, dict):
|
|
412
|
+
errors = '\n'.join(f'{k}={v}' for k, v in request.errors.items())
|
|
413
|
+
elif isinstance(request.errors, (list, tuple)):
|
|
414
|
+
errors = '\n'.join(str(error) for error in request.errors)
|
|
415
|
+
else:
|
|
416
|
+
errors = str(request.errors)
|
|
387
417
|
message = f'{message}\nErrors:\n{errors}'
|
|
388
418
|
return message
|
|
389
419
|
|
|
@@ -639,6 +669,16 @@ class BaseJsonResource(BaseResource):
|
|
|
639
669
|
)
|
|
640
670
|
|
|
641
671
|
|
|
672
|
+
def _as_result_bound(max_results):
|
|
673
|
+
"""The caller's result bound, or None when there isn't one.
|
|
674
|
+
|
|
675
|
+
None, 0 and negatives all mean "no bound".
|
|
676
|
+
"""
|
|
677
|
+
if not max_results or max_results < 0:
|
|
678
|
+
return None
|
|
679
|
+
return max_results
|
|
680
|
+
|
|
681
|
+
|
|
642
682
|
# ── Hash helpers ───────────────────────────────────────────────────
|
|
643
683
|
|
|
644
684
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PolyswarmException(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
#########################################
|
|
11
|
+
# API layer exceptions
|
|
12
|
+
#########################################
|
|
13
|
+
|
|
14
|
+
class PolyswarmAPIException(PolyswarmException):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimeoutException(PolyswarmAPIException):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
#########################################
|
|
23
|
+
# Request layer exceptions
|
|
24
|
+
#########################################
|
|
25
|
+
|
|
26
|
+
class RequestException(PolyswarmException):
|
|
27
|
+
def __init__(self, request, *args):
|
|
28
|
+
super().__init__(*args)
|
|
29
|
+
self.request = request
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UsageLimitsExceededException(RequestException):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotFoundException(RequestException):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalise_sources(sources):
|
|
41
|
+
"""Coerce a server-supplied ``sources`` payload into a list of feed names.
|
|
42
|
+
|
|
43
|
+
``.sources`` is published as a list of strings and consumers iterate it, so a
|
|
44
|
+
bare ``'nsrl'`` cannot be handed through — it would iterate as characters.
|
|
45
|
+
The server sends a list of strings today; a feed-dict entry (the shape the
|
|
46
|
+
instance-level ``known_good`` field uses) yields ``feed['tool']`` defensively
|
|
47
|
+
rather than being dropped. Anything unrecognised becomes ``[]`` and is logged,
|
|
48
|
+
so a discard is never silent. The raw payload stays at ``exc.request.errors``.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(sources, str):
|
|
51
|
+
return [sources]
|
|
52
|
+
if isinstance(sources, list):
|
|
53
|
+
names = []
|
|
54
|
+
for source in sources:
|
|
55
|
+
if isinstance(source, str):
|
|
56
|
+
names.append(source)
|
|
57
|
+
elif isinstance(source, dict) and isinstance(source.get('tool'), str):
|
|
58
|
+
names.append(source['tool'])
|
|
59
|
+
else:
|
|
60
|
+
logger.warning('Dropping unrecognised known-good sources entry: %r', source)
|
|
61
|
+
return names
|
|
62
|
+
if sources is not None:
|
|
63
|
+
logger.warning('Dropping unrecognised known-good sources payload: %r', sources)
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class KnownGoodWithheldException(NotFoundException):
|
|
68
|
+
"""404 for a known-good binary: the platform never stores or serves its bytes.
|
|
69
|
+
|
|
70
|
+
Subclasses ``NotFoundException`` so every existing ``except NotFoundException``
|
|
71
|
+
handler keeps working unchanged; catch this subclass only when the caller wants
|
|
72
|
+
to tell "withheld by design" apart from a plain "not found". The metadata the
|
|
73
|
+
platform holds about the artifact is still readable through the search /
|
|
74
|
+
instance endpoints.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, request, *args, sources=None):
|
|
78
|
+
super().__init__(request, *args)
|
|
79
|
+
# Known-good feeds that flagged the hash (e.g. ``['nsrl']``); always a list
|
|
80
|
+
# of strings — normalised here, at the boundary, so the documented shape
|
|
81
|
+
# holds whatever the envelope carried. Empty when the server named none.
|
|
82
|
+
self.sources = _normalise_sources(sources)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class NoResultsException(RequestException):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class FailedInstanceException(RequestException):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#########################################
|
|
94
|
+
# Types layer exceptions
|
|
95
|
+
#########################################
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TypeException(PolyswarmException):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MissingAPIInstanceException(TypeException):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InvalidJSONResponseException(TypeException):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DecodeErrorException(TypeException):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class InvalidValueException(TypeException):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ArtifactDeletedException(TypeException):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class InvalidYaraRulesException(TypeException):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NotImportedException(TypeException):
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
#########################################
|
|
131
|
+
# Warnings
|
|
132
|
+
#########################################
|
|
133
|
+
|
|
134
|
+
class APIWarning(Warning):
|
|
135
|
+
pass
|
|
@@ -275,9 +275,14 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
275
275
|
self.known_good_sources = sorted(
|
|
276
276
|
{feed['tool'] for feed in self.known_good if feed.get('tool')}
|
|
277
277
|
) if self.known_good else []
|
|
278
|
-
# Friendly
|
|
279
|
-
#
|
|
280
|
-
#
|
|
278
|
+
# Friendly REPORTED-state NAME (e.g. 'KNOWN_GOOD' / 'SETTLED' / 'STORED' /
|
|
279
|
+
# 'NOT_STORED'). The server derives it from its two-predicate known-good model
|
|
280
|
+
# (artifact-index specs/05): 'KNOWN_GOOD' while the file is currently known-good,
|
|
281
|
+
# 'NOT_STORED' for a submission it declined as known-good whose hash is no longer
|
|
282
|
+
# currently known-good (nothing was ever stored; a fresh submit works), otherwise
|
|
283
|
+
# the instance's own persisted state. Additive alongside the numeric
|
|
284
|
+
# bounty_state, and optional — older servers omit it, so .get() yields None
|
|
285
|
+
# (no behaviour change).
|
|
281
286
|
self.state = content.get('state')
|
|
282
287
|
|
|
283
288
|
# ArtifactInstance fields
|
|
@@ -741,12 +746,51 @@ class YaraRuleset(core.BaseJsonResource):
|
|
|
741
746
|
self.modified = core.parse_isoformat(content.get('modified'))
|
|
742
747
|
self.deleted = content.get('deleted')
|
|
743
748
|
self.yara = content.get('yara')
|
|
749
|
+
# Hunt-page tracking fields. All additive — an older server simply
|
|
750
|
+
# leaves them None.
|
|
751
|
+
self.favorite = content.get('favorite')
|
|
752
|
+
self.favorited_at = core.parse_isoformat(content.get('favorited_at'))
|
|
753
|
+
# Number of rules in the ruleset body. None means "no answer" (the
|
|
754
|
+
# server could not or has not counted), which is different from 0.
|
|
755
|
+
self.rule_count = content.get('rule_count')
|
|
756
|
+
self.historical_hunt_count = content.get('historical_hunt_count')
|
|
757
|
+
# Live results collected in the product window — a STORED counter the
|
|
758
|
+
# server refreshes on a schedule, always present on list rows with a
|
|
759
|
+
# running live hunt. None means "not yet refreshed / no live hunt",
|
|
760
|
+
# which is different from 0 ("refreshed, nothing new").
|
|
761
|
+
self.new_results_count = content.get('new_results_count')
|
|
762
|
+
# When the server last refreshed the counter — the staleness marker
|
|
763
|
+
# that makes a stored count trustworthy (None iff the count is None).
|
|
764
|
+
self.new_results_counted_at = core.parse_isoformat(
|
|
765
|
+
content.get('new_results_counted_at'))
|
|
744
766
|
|
|
745
767
|
|
|
746
768
|
class LiveYaraRuleset(YaraRuleset):
|
|
747
769
|
RESOURCE_ENDPOINT = '/hunt/rule/live'
|
|
748
770
|
|
|
749
771
|
|
|
772
|
+
class YaraRulesetFavorite(core.BaseJsonResource):
|
|
773
|
+
"""The favorite-toggle response: the ruleset's new star state plus the
|
|
774
|
+
team's budget usage, so callers can render "N of M used" without counting
|
|
775
|
+
client-side."""
|
|
776
|
+
RESOURCE_ENDPOINT = '/hunt/rule/favorite'
|
|
777
|
+
# The toggle's payload ({id, favorite}) rides the JSON body — the server
|
|
778
|
+
# reads BOTH from the body — so the default RESOURCE_ID_KEYS (['id'])
|
|
779
|
+
# would move `id` into the query string and the server would 400 with
|
|
780
|
+
# "A valid rule id must be provided". `community` stays a query-string
|
|
781
|
+
# key to match where every ruleset GET/list sends it (the server accepts
|
|
782
|
+
# it from either the query or the body, but never from both at once).
|
|
783
|
+
RESOURCE_ID_KEYS = ['community']
|
|
784
|
+
|
|
785
|
+
def __init__(self, content, api=None):
|
|
786
|
+
super().__init__(content, api=api)
|
|
787
|
+
self.id = content.get('id')
|
|
788
|
+
self.favorite = content.get('favorite')
|
|
789
|
+
self.favorited_at = core.parse_isoformat(content.get('favorited_at'))
|
|
790
|
+
self.favorites_used = content.get('favorites_used')
|
|
791
|
+
self.favorites_limit = content.get('favorites_limit')
|
|
792
|
+
|
|
793
|
+
|
|
750
794
|
class LiveHuntResult(core.BaseJsonResource):
|
|
751
795
|
RESOURCE_ENDPOINT = '/hunt/live'
|
|
752
796
|
|
|
@@ -789,6 +833,16 @@ class HistoricalHunt(core.BaseJsonResource):
|
|
|
789
833
|
self.progress = content['progress']
|
|
790
834
|
self.results_csv_uri = content['results_csv_uri']
|
|
791
835
|
self.communities = content.get('communities')
|
|
836
|
+
# Source-rule provenance. rule_id is the ruleset this hunt was
|
|
837
|
+
# triggered from (None for raw-yara hunts and hunts predating the
|
|
838
|
+
# tracking); rule_modified is a freeze-time audit timestamp.
|
|
839
|
+
self.rule_id = content.get('rule_id')
|
|
840
|
+
self.rule_modified = core.parse_isoformat(content.get('rule_modified'))
|
|
841
|
+
# Tri-state: has the source ruleset's body changed SINCE THE HUNT
|
|
842
|
+
# FROZE IT? True/False when the server could compare; None means
|
|
843
|
+
# unknown (no source rule, or nothing to compare against) — not
|
|
844
|
+
# "unchanged".
|
|
845
|
+
self.source_rule_changed = content.get('source_rule_changed')
|
|
792
846
|
|
|
793
847
|
|
|
794
848
|
class HistoricalHuntList(HistoricalHunt):
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
class PolyswarmException(Exception):
|
|
2
|
-
pass
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
#########################################
|
|
6
|
-
# API layer exceptions
|
|
7
|
-
#########################################
|
|
8
|
-
|
|
9
|
-
class PolyswarmAPIException(PolyswarmException):
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TimeoutException(PolyswarmAPIException):
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#########################################
|
|
18
|
-
# Request layer exceptions
|
|
19
|
-
#########################################
|
|
20
|
-
|
|
21
|
-
class RequestException(PolyswarmException):
|
|
22
|
-
def __init__(self, request, *args):
|
|
23
|
-
super().__init__(*args)
|
|
24
|
-
self.request = request
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class UsageLimitsExceededException(RequestException):
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class NotFoundException(RequestException):
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class NoResultsException(RequestException):
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class FailedInstanceException(RequestException):
|
|
40
|
-
pass
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
#########################################
|
|
44
|
-
# Types layer exceptions
|
|
45
|
-
#########################################
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class TypeException(PolyswarmException):
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class MissingAPIInstanceException(TypeException):
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class InvalidJSONResponseException(TypeException):
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class DecodeErrorException(TypeException):
|
|
61
|
-
pass
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class InvalidValueException(TypeException):
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class ArtifactDeletedException(TypeException):
|
|
69
|
-
pass
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class InvalidYaraRulesException(TypeException):
|
|
73
|
-
pass
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class NotImportedException(TypeException):
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#########################################
|
|
81
|
-
# Warnings
|
|
82
|
-
#########################################
|
|
83
|
-
|
|
84
|
-
class APIWarning(Warning):
|
|
85
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|