polyswarm-api 4.3.0__tar.gz → 4.5.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.5.0}/PKG-INFO +1 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/pyproject.toml +2 -2
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/__init__.py +1 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/aio/api.py +84 -5
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/api.py +100 -4
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/core.py +12 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/resources.py +59 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0/src/polyswarm_api.egg-info}/PKG-INFO +1 -1
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/LICENSE +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/README.md +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/setup.cfg +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/aio/__init__.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/aio/session.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/exceptions.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/session.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api/settings.py +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api.egg-info/SOURCES.txt +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api.egg-info/dependency_links.txt +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.0}/src/polyswarm_api.egg-info/requires.txt +0 -0
- {polyswarm_api-4.3.0 → polyswarm_api-4.5.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.5.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.5.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,82 @@ 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, sort=None,
|
|
701
|
+
exclude_favorites=None):
|
|
688
702
|
"""
|
|
689
703
|
List all YaraRulesets for the current account.
|
|
704
|
+
|
|
705
|
+
All filters are optional and conjunctive:
|
|
706
|
+
:param name: Case-insensitive substring match on the ruleset name.
|
|
707
|
+
:param status: 'active' returns only rulesets whose live hunt is
|
|
708
|
+
currently running.
|
|
709
|
+
:param favorites_only: True returns only favorited rulesets.
|
|
710
|
+
:param exclude_favorites: True returns only the rulesets that are NOT
|
|
711
|
+
favorited — the inverse of ``favorites_only``, and refused together
|
|
712
|
+
with it (a contradiction, answered with an error rather than an
|
|
713
|
+
empty list). It exists for clients that render the favorites as
|
|
714
|
+
their own list: the favorites are a separate, unpaginated fetch
|
|
715
|
+
bounded by the account's budget, so leaving them in the paginated
|
|
716
|
+
list too makes a page either repeat a row or come back short.
|
|
717
|
+
Appended to the signature rather than placed beside
|
|
718
|
+
``favorites_only`` so a positional caller keeps working.
|
|
719
|
+
:param has_new_results: True returns only rulesets whose stored
|
|
720
|
+
new-results counter is positive. The counter (and its window) is
|
|
721
|
+
maintained server-side by a scheduled refresh; rows carry it as
|
|
722
|
+
``new_results_count`` with ``new_results_counted_at`` marking when
|
|
723
|
+
it was last refreshed. There is no per-request window parameter.
|
|
724
|
+
:param sort: ``'active_first'`` returns the rulesets that carry a live
|
|
725
|
+
hunt link first, newest first within each block. Default (None) is
|
|
726
|
+
newest first. "Newest first" is the server's own insertion key, NOT
|
|
727
|
+
the ``id`` on the rows you get back — that one is unique but
|
|
728
|
+
unordered, so dedupe with it and never resume or bound a walk with
|
|
729
|
+
it. Applied SERVER-side, across pages — the list is
|
|
730
|
+
keyset-paginated, so a client-side sort would only ever reorder one
|
|
731
|
+
page; the SDK never re-orders rows. Reuse a page's ``offset`` only
|
|
732
|
+
with the same ``sort``: the server refuses a cursor minted under
|
|
733
|
+
the other order.
|
|
734
|
+
|
|
735
|
+
Two server-side properties of that key, neither of them SDK
|
|
736
|
+
behaviour. It ranks on the stored link, which is a WIDER predicate
|
|
737
|
+
than the one ``livescan_id`` is rendered under: a legacy row whose
|
|
738
|
+
hunt was stopped without clearing the link ranks in the leading
|
|
739
|
+
block while still serializing ``livescan_id`` as ``None``. Read the
|
|
740
|
+
field to decide whether a ruleset is running; never the position.
|
|
741
|
+
|
|
742
|
+
And the key is MUTABLE, unlike that default: a ruleset whose
|
|
743
|
+
live hunt stops mid-walk falls back into the idle block below the
|
|
744
|
+
cursor and is yielded twice, and one started mid-walk moves above
|
|
745
|
+
the cursor and is skipped for the rest of that walk. That is a
|
|
746
|
+
property of the walk, so starting fresh from the first page does
|
|
747
|
+
not avoid it. This generator streams pages and does not dedupe —
|
|
748
|
+
dedupe by ``id`` if you consume more than one page.
|
|
690
749
|
:return: A generator of YaraRuleset resources
|
|
691
750
|
"""
|
|
692
751
|
logger.info('List rulesets')
|
|
693
|
-
async for item in self._paginate(resources.YaraRuleset.list(
|
|
752
|
+
async for item in self._paginate(resources.YaraRuleset.list(
|
|
753
|
+
self, name=name, status=status, favorites_only=favorites_only,
|
|
754
|
+
has_new_results=has_new_results, sort=sort,
|
|
755
|
+
exclude_favorites=exclude_favorites,
|
|
756
|
+
community=self.community)):
|
|
694
757
|
yield item
|
|
695
758
|
|
|
759
|
+
async def ruleset_favorite(self, ruleset_id, favorite=True):
|
|
760
|
+
"""
|
|
761
|
+
Favorite or unfavorite a YaraRuleset. Idempotent; works while a live
|
|
762
|
+
hunt is running. Favorites are shared by the whole team and capped
|
|
763
|
+
(the response carries favorites_used / favorites_limit); when the
|
|
764
|
+
budget is exhausted the server refuses with a machine-readable
|
|
765
|
+
FAVORITE_LIMIT error.
|
|
766
|
+
|
|
767
|
+
:param ruleset_id: Id of the ruleset
|
|
768
|
+
:param favorite: True to star, False to unstar
|
|
769
|
+
:return: A YaraRulesetFavorite resource
|
|
770
|
+
"""
|
|
771
|
+
logger.info('%s ruleset %s', 'Favorite' if favorite else 'Unfavorite', ruleset_id)
|
|
772
|
+
return await self._single(resources.YaraRulesetFavorite.update(
|
|
773
|
+
self, id=ruleset_id, favorite=favorite, community=self.community))
|
|
774
|
+
|
|
696
775
|
async def tag_link_get(self, sha256):
|
|
697
776
|
"""
|
|
698
777
|
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,100 @@ 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,
|
|
841
|
+
name=None,
|
|
842
|
+
status=None,
|
|
843
|
+
favorites_only=None,
|
|
844
|
+
has_new_results=None,
|
|
845
|
+
sort=None,
|
|
846
|
+
exclude_favorites=None,
|
|
847
|
+
):
|
|
827
848
|
"""
|
|
828
849
|
List all YaraRulesets for the current account.
|
|
850
|
+
|
|
851
|
+
All filters are optional and conjunctive:
|
|
852
|
+
:param name: Case-insensitive substring match on the ruleset name.
|
|
853
|
+
:param status: 'active' returns only rulesets whose live hunt is
|
|
854
|
+
currently running.
|
|
855
|
+
:param favorites_only: True returns only favorited rulesets.
|
|
856
|
+
:param exclude_favorites: True returns only the rulesets that are NOT
|
|
857
|
+
favorited — the inverse of ``favorites_only``, and refused together
|
|
858
|
+
with it (a contradiction, answered with an error rather than an
|
|
859
|
+
empty list). It exists for clients that render the favorites as
|
|
860
|
+
their own list: the favorites are a separate, unpaginated fetch
|
|
861
|
+
bounded by the account's budget, so leaving them in the paginated
|
|
862
|
+
list too makes a page either repeat a row or come back short.
|
|
863
|
+
Appended to the signature rather than placed beside
|
|
864
|
+
``favorites_only`` so a positional caller keeps working.
|
|
865
|
+
:param has_new_results: True returns only rulesets whose stored
|
|
866
|
+
new-results counter is positive. The counter (and its window) is
|
|
867
|
+
maintained server-side by a scheduled refresh; rows carry it as
|
|
868
|
+
``new_results_count`` with ``new_results_counted_at`` marking when
|
|
869
|
+
it was last refreshed. There is no per-request window parameter.
|
|
870
|
+
:param sort: ``'active_first'`` returns the rulesets that carry a live
|
|
871
|
+
hunt link first, newest first within each block. Default (None) is
|
|
872
|
+
newest first. "Newest first" is the server's own insertion key, NOT
|
|
873
|
+
the ``id`` on the rows you get back — that one is unique but
|
|
874
|
+
unordered, so dedupe with it and never resume or bound a walk with
|
|
875
|
+
it. Applied SERVER-side, across pages — the list is
|
|
876
|
+
keyset-paginated, so a client-side sort would only ever reorder one
|
|
877
|
+
page; the SDK never re-orders rows. Reuse a page's ``offset`` only
|
|
878
|
+
with the same ``sort``: the server refuses a cursor minted under
|
|
879
|
+
the other order.
|
|
880
|
+
|
|
881
|
+
Two server-side properties of that key, neither of them SDK
|
|
882
|
+
behaviour. It ranks on the stored link, which is a WIDER predicate
|
|
883
|
+
than the one ``livescan_id`` is rendered under: a legacy row whose
|
|
884
|
+
hunt was stopped without clearing the link ranks in the leading
|
|
885
|
+
block while still serializing ``livescan_id`` as ``None``. Read the
|
|
886
|
+
field to decide whether a ruleset is running; never the position.
|
|
887
|
+
|
|
888
|
+
And the key is MUTABLE, unlike that default: a ruleset whose
|
|
889
|
+
live hunt stops mid-walk falls back into the idle block below the
|
|
890
|
+
cursor and is yielded twice, and one started mid-walk moves above
|
|
891
|
+
the cursor and is skipped for the rest of that walk. That is a
|
|
892
|
+
property of the walk, so starting fresh from the first page does
|
|
893
|
+
not avoid it. This generator streams pages and does not dedupe —
|
|
894
|
+
dedupe by ``id`` if you consume more than one page.
|
|
829
895
|
:return: A generator of YaraRuleset resources
|
|
830
896
|
"""
|
|
831
897
|
logger.info("List rulesets")
|
|
832
898
|
for item in self._paginate(
|
|
833
|
-
resources.YaraRuleset.list(
|
|
899
|
+
resources.YaraRuleset.list(
|
|
900
|
+
self,
|
|
901
|
+
name=name,
|
|
902
|
+
status=status,
|
|
903
|
+
favorites_only=favorites_only,
|
|
904
|
+
has_new_results=has_new_results,
|
|
905
|
+
sort=sort,
|
|
906
|
+
exclude_favorites=exclude_favorites,
|
|
907
|
+
community=self.community,
|
|
908
|
+
)
|
|
834
909
|
):
|
|
835
910
|
yield item
|
|
836
911
|
|
|
912
|
+
def ruleset_favorite(self, ruleset_id, favorite=True):
|
|
913
|
+
"""
|
|
914
|
+
Favorite or unfavorite a YaraRuleset. Idempotent; works while a live
|
|
915
|
+
hunt is running. Favorites are shared by the whole team and capped
|
|
916
|
+
(the response carries favorites_used / favorites_limit); when the
|
|
917
|
+
budget is exhausted the server refuses with a machine-readable
|
|
918
|
+
FAVORITE_LIMIT error.
|
|
919
|
+
|
|
920
|
+
:param ruleset_id: Id of the ruleset
|
|
921
|
+
:param favorite: True to star, False to unstar
|
|
922
|
+
:return: A YaraRulesetFavorite resource
|
|
923
|
+
"""
|
|
924
|
+
logger.info(
|
|
925
|
+
"%s ruleset %s", "Favorite" if favorite else "Unfavorite", ruleset_id
|
|
926
|
+
)
|
|
927
|
+
return self._single(
|
|
928
|
+
resources.YaraRulesetFavorite.update(
|
|
929
|
+
self, id=ruleset_id, favorite=favorite, community=self.community
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
|
|
837
933
|
def tag_link_get(self, sha256):
|
|
838
934
|
"""
|
|
839
935
|
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
|
|
|
@@ -766,6 +805,11 @@ class LiveHuntResult(core.BaseJsonResource):
|
|
|
766
805
|
self.sha1 = content.get('sha1')
|
|
767
806
|
self.rule_name = content['rule_name']
|
|
768
807
|
self.tags = content['tags']
|
|
808
|
+
# `.get()`, not a subscript -- both keys are additive, so an older server omits
|
|
809
|
+
# them. None is AMBIGUOUS on both (four causes) and is never a claim that the
|
|
810
|
+
# evidence is complete. Contract: specs/05-downstream-contract.md.
|
|
811
|
+
self.matched_strings = content.get('matched_strings')
|
|
812
|
+
self.matched_strings_dropped = content.get('matched_strings_dropped')
|
|
769
813
|
self.polyscore = content['polyscore']
|
|
770
814
|
self.malware_family = content['malware_family']
|
|
771
815
|
self.detections = content['detections']
|
|
@@ -794,6 +838,16 @@ class HistoricalHunt(core.BaseJsonResource):
|
|
|
794
838
|
self.progress = content['progress']
|
|
795
839
|
self.results_csv_uri = content['results_csv_uri']
|
|
796
840
|
self.communities = content.get('communities')
|
|
841
|
+
# Source-rule provenance. rule_id is the ruleset this hunt was
|
|
842
|
+
# triggered from (None for raw-yara hunts and hunts predating the
|
|
843
|
+
# tracking); rule_modified is a freeze-time audit timestamp.
|
|
844
|
+
self.rule_id = content.get('rule_id')
|
|
845
|
+
self.rule_modified = core.parse_isoformat(content.get('rule_modified'))
|
|
846
|
+
# Tri-state: has the source ruleset's body changed SINCE THE HUNT
|
|
847
|
+
# FROZE IT? True/False when the server could compare; None means
|
|
848
|
+
# unknown (no source rule, or nothing to compare against) — not
|
|
849
|
+
# "unchanged".
|
|
850
|
+
self.source_rule_changed = content.get('source_rule_changed')
|
|
797
851
|
|
|
798
852
|
|
|
799
853
|
class HistoricalHuntList(HistoricalHunt):
|
|
@@ -814,6 +868,11 @@ class HistoricalHuntResult(core.BaseJsonResource):
|
|
|
814
868
|
self.created = core.parse_isoformat(content['created'])
|
|
815
869
|
self.rule_name = content['rule_name']
|
|
816
870
|
self.tags = content['tags']
|
|
871
|
+
# `.get()`, not a subscript -- both keys are additive, so an older server omits
|
|
872
|
+
# them. None is AMBIGUOUS on both (four causes) and is never a claim that the
|
|
873
|
+
# evidence is complete. Contract: specs/05-downstream-contract.md.
|
|
874
|
+
self.matched_strings = content.get('matched_strings')
|
|
875
|
+
self.matched_strings_dropped = content.get('matched_strings_dropped')
|
|
817
876
|
self.polyscore = content['polyscore']
|
|
818
877
|
self.malware_family = content['malware_family']
|
|
819
878
|
self.detections = content['detections']
|
|
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
|