polyswarm-api 4.3.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.3.0/src/polyswarm_api.egg-info → polyswarm_api-4.4.0}/PKG-INFO +1 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/pyproject.toml +2 -2
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/__init__.py +1 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/api.py +47 -5
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/api.py +58 -4
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/core.py +12 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/resources.py +49 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0/src/polyswarm_api.egg-info}/PKG-INFO +1 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/LICENSE +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/README.md +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/setup.cfg +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/__init__.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/aio/session.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/exceptions.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/session.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api/settings.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/SOURCES.txt +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/dependency_links.txt +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.4.0}/src/polyswarm_api.egg-info/requires.txt +0 -0
- {polyswarm_api-4.3.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
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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
|
"""
|
|
@@ -668,6 +669,16 @@ class BaseJsonResource(BaseResource):
|
|
|
668
669
|
)
|
|
669
670
|
|
|
670
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
|
+
|
|
671
682
|
# ── Hash helpers ───────────────────────────────────────────────────
|
|
672
683
|
|
|
673
684
|
|
|
@@ -746,12 +746,51 @@ class YaraRuleset(core.BaseJsonResource):
|
|
|
746
746
|
self.modified = core.parse_isoformat(content.get('modified'))
|
|
747
747
|
self.deleted = content.get('deleted')
|
|
748
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'))
|
|
749
766
|
|
|
750
767
|
|
|
751
768
|
class LiveYaraRuleset(YaraRuleset):
|
|
752
769
|
RESOURCE_ENDPOINT = '/hunt/rule/live'
|
|
753
770
|
|
|
754
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
|
+
|
|
755
794
|
class LiveHuntResult(core.BaseJsonResource):
|
|
756
795
|
RESOURCE_ENDPOINT = '/hunt/live'
|
|
757
796
|
|
|
@@ -794,6 +833,16 @@ class HistoricalHunt(core.BaseJsonResource):
|
|
|
794
833
|
self.progress = content['progress']
|
|
795
834
|
self.results_csv_uri = content['results_csv_uri']
|
|
796
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')
|
|
797
846
|
|
|
798
847
|
|
|
799
848
|
class HistoricalHuntList(HistoricalHunt):
|
|
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
|
|
File without changes
|