ps-banshee 1.1.0__py3-none-any.whl

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.
Files changed (78) hide show
  1. banshee/__init__.py +16 -0
  2. banshee/_version.py +15 -0
  3. banshee/app_config.py +40 -0
  4. banshee/branding.py +36 -0
  5. banshee/commands/__init__.py +12 -0
  6. banshee/commands/args.py +44 -0
  7. banshee/commands/cmd_classic_alerts.py +175 -0
  8. banshee/commands/cmd_entity.py +54 -0
  9. banshee/commands/cmd_ioc.py +236 -0
  10. banshee/commands/cmd_lists.py +293 -0
  11. banshee/commands/cmd_pcap_enrich.py +74 -0
  12. banshee/commands/cmd_playbook_alerts.py +297 -0
  13. banshee/commands/cmd_risklist.py +239 -0
  14. banshee/commands/cmd_rules.py +185 -0
  15. banshee/commands/epilogs.py +602 -0
  16. banshee/commands/errors.py +20 -0
  17. banshee/detection_rules/__init__.py +14 -0
  18. banshee/detection_rules/detection_rules_search.py +210 -0
  19. banshee/entity_match/__init__.py +17 -0
  20. banshee/entity_match/constants.py +168 -0
  21. banshee/entity_match/errors.py +20 -0
  22. banshee/entity_match/lookup.py +42 -0
  23. banshee/entity_match/search.py +52 -0
  24. banshee/formatters/__init__.py +12 -0
  25. banshee/formatters/output_formatters.py +45 -0
  26. banshee/fusion_files/__init__.py +14 -0
  27. banshee/fusion_files/feed_stat.py +66 -0
  28. banshee/indicators/__init__.py +18 -0
  29. banshee/indicators/constants.py +103 -0
  30. banshee/indicators/helpers.py +24 -0
  31. banshee/indicators/lookup.py +92 -0
  32. banshee/indicators/rules.py +82 -0
  33. banshee/indicators/search.py +80 -0
  34. banshee/indicators/soar.py +57 -0
  35. banshee/legacy_alerts/__init__.py +12 -0
  36. banshee/legacy_alerts/alert_lookup.py +38 -0
  37. banshee/legacy_alerts/alert_search.py +82 -0
  38. banshee/legacy_alerts/alert_update.py +80 -0
  39. banshee/legacy_alerts/constants.py +35 -0
  40. banshee/legacy_alerts/rules_search.py +46 -0
  41. banshee/lists/__init__.py +24 -0
  42. banshee/lists/fetch_list.py +35 -0
  43. banshee/lists/list_add.py +53 -0
  44. banshee/lists/list_bulk_add.py +85 -0
  45. banshee/lists/list_bulk_remove.py +85 -0
  46. banshee/lists/list_clear.py +31 -0
  47. banshee/lists/list_create.py +46 -0
  48. banshee/lists/list_entities.py +42 -0
  49. banshee/lists/list_entries.py +37 -0
  50. banshee/lists/list_helpers.py +100 -0
  51. banshee/lists/list_info.py +39 -0
  52. banshee/lists/list_remove.py +46 -0
  53. banshee/lists/list_search.py +57 -0
  54. banshee/lists/list_status.py +37 -0
  55. banshee/main.py +118 -0
  56. banshee/pcap_enrich/__init__.py +14 -0
  57. banshee/pcap_enrich/constants.py +18 -0
  58. banshee/pcap_enrich/helpers.py +64 -0
  59. banshee/pcap_enrich/pcap_enrich.py +251 -0
  60. banshee/playbook_alerts/__init__.py +14 -0
  61. banshee/playbook_alerts/alert_lookup.py +49 -0
  62. banshee/playbook_alerts/alert_search.py +62 -0
  63. banshee/playbook_alerts/alert_update.py +107 -0
  64. banshee/playbook_alerts/constants.py +75 -0
  65. banshee/risklist/__init__.py +17 -0
  66. banshee/risklist/risklist_create.py +154 -0
  67. banshee/risklist/risklist_fetch.py +89 -0
  68. banshee/risklist/risklist_stat.py +58 -0
  69. banshee/threat/__init__.py +16 -0
  70. banshee/threat/constants.py +99 -0
  71. banshee/threat/endpoints.py +20 -0
  72. banshee/threat/fetch_threat_map.py +59 -0
  73. ps_banshee-1.1.0.dist-info/METADATA +146 -0
  74. ps_banshee-1.1.0.dist-info/RECORD +78 -0
  75. ps_banshee-1.1.0.dist-info/WHEEL +5 -0
  76. ps_banshee-1.1.0.dist-info/entry_points.txt +2 -0
  77. ps_banshee-1.1.0.dist-info/licenses/LICENSE +21 -0
  78. ps_banshee-1.1.0.dist-info/top_level.txt +1 -0
banshee/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ """Base package for PS Banshee."""
15
+
16
+ from ._version import __version__ as version
banshee/_version.py ADDED
@@ -0,0 +1,15 @@
1
+ #################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+ from importlib.metadata import version
14
+
15
+ __version__ = version('ps-banshee')
banshee/app_config.py ADDED
@@ -0,0 +1,40 @@
1
+ #################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+
15
+ from psengine.config import Config
16
+ from pydantic import ValidationError
17
+
18
+ from ._version import __version__
19
+ from .commands.errors import InitConfigError
20
+
21
+
22
+ def config_init(cmd: str, rf_token: str = None, no_ssl_verify: bool = False) -> Config:
23
+ """Global configuration for the CLI.
24
+
25
+ Args:
26
+ cmd (str): The command name + sub command, used to generate the app_id,
27
+ typically will be the name one of the banshee commands,
28
+ confor example: 'ca-search', or 'entity-lookup'.
29
+ rf_token (str, optional): The Recorded Future API token.
30
+ no_ssl_verify (bool, optional): Disable SSL verification.
31
+ """
32
+ # invert no_ssl_verify
33
+ ssl_verify = not no_ssl_verify
34
+ app_id = f'banshee_{cmd}/{__version__}'
35
+ try:
36
+ Config.init(rf_token=rf_token, app_id=app_id, client_ssl_verify=ssl_verify)
37
+ except ValidationError as e:
38
+ if 'rf_token' in e.errors()[0]['loc']:
39
+ raise InitConfigError('Invalid Recorded Future API key') # noqa: B904
40
+ raise InitConfigError(e.errors()[0]['msg']) # noqa: B904
banshee/branding.py ADDED
@@ -0,0 +1,36 @@
1
+ #################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+
15
+ from typer import Typer
16
+
17
+ BRANDING = ':rocket: \033[93mBrought to you by the Cyber Security Engineers at Recorded Future\033[0m :rocket:' # noqa: E501
18
+
19
+
20
+ def banshee_cmd(app: Typer, help_: str, epilog: str, *args, **kwargs):
21
+ """Main decorator create banshee commands.
22
+ Under the hood, it adds branding to the help text of the command.
23
+
24
+
25
+ Args:
26
+ app (Typer): The Typer app instance
27
+ help_ (str): The help text for the command
28
+ epilog (str): The epilog text for the command
29
+ *args: Additional arguments to pass to the command
30
+ **kwargs: Additional keyword arguments to pass to the command
31
+
32
+ Returns:
33
+ Typer: The updated Typer app instance
34
+ """
35
+ help_ += f'\n\n{BRANDING}'
36
+ return app.command(help=help_, epilog=epilog, *args, **kwargs) # noqa: B026
@@ -0,0 +1,12 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
@@ -0,0 +1,44 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ from typing import Annotated, Optional
15
+
16
+ from typer import Option
17
+
18
+ ################################
19
+ # Global options / arguments
20
+ ################################
21
+
22
+ # How to use: api_key: RF_API_KEY = None
23
+ OPT_RF_API_KEY = Annotated[
24
+ Optional[str],
25
+ Option(
26
+ '--api-key', '-k', help='Recorded Future API Key', envvar='RF_TOKEN', show_default=False
27
+ ),
28
+ ]
29
+
30
+ # How to use: pretty: PRETTY_PRINT = False
31
+ OPT_PRETTY_PRINT = Annotated[
32
+ bool, Option('--pretty', '-p', help='Pretty print the results in a human readable format')
33
+ ]
34
+
35
+
36
+ OPT_NO_SSL_VERIFY = Annotated[
37
+ Optional[bool],
38
+ Option(
39
+ '--no-ssl-verify',
40
+ '-s',
41
+ help="""Disable SSL Verification. Useful when using proxies. To
42
+ utilize a proxy set the environment variable HTTP_PROXY or HTTPS_PROXY.""",
43
+ ),
44
+ ]
@@ -0,0 +1,175 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ import re
15
+ import sys
16
+ from typing import Annotated
17
+
18
+ from typer import Argument, BadParameter, Option, Typer
19
+
20
+ from ..branding import banshee_cmd
21
+ from ..legacy_alerts.alert_lookup import lookup_alert
22
+ from ..legacy_alerts.alert_search import search_alerts
23
+ from ..legacy_alerts.alert_update import update_alerts
24
+ from ..legacy_alerts.constants import AlertStatus
25
+ from ..legacy_alerts.rules_search import search_alert_rules
26
+ from .args import OPT_PRETTY_PRINT
27
+ from .epilogs import (
28
+ EPILOG_ALERT_LOOKUP,
29
+ EPILOG_ALERT_RULES_SEARCH,
30
+ EPILOG_ALERT_SEARCH,
31
+ EPILOG_ALERT_UPDATE,
32
+ )
33
+
34
+ CMD_NAME = 'ca'
35
+ CMD_HELP = 'Search and lookup Classic Alerts'
36
+ CMD_RICH_HELP = 'Recorded Future Classic Alerts'
37
+
38
+ app = Typer(no_args_is_help=True)
39
+
40
+ ALERT_ID_INVALID_MSG = "Alert ID '{}' is not valid. Alert ID should be at least 6 characters long." # noqa: E501
41
+
42
+ ###################################
43
+ # Callbacks
44
+ ###################################
45
+
46
+
47
+ def validate_alert_id(alert_id: str):
48
+ # if value is less than 6 char long
49
+ if len(alert_id) < 6:
50
+ raise BadParameter(ALERT_ID_INVALID_MSG.format(alert_id))
51
+
52
+ return alert_id
53
+
54
+
55
+ def parse_alert_ids_input(value: list[str]):
56
+ if not value:
57
+ raise BadParameter('No Alert IDs supplied')
58
+
59
+ alert_ids = value
60
+ if isinstance(value, str):
61
+ alert_ids = [x for x in re.split(r'[\s]+', value) if x]
62
+
63
+ if not len(alert_ids):
64
+ raise BadParameter('No Alert IDs provided')
65
+
66
+ # Now check that each ID is valid
67
+ for alert_id in alert_ids:
68
+ validate_alert_id(alert_id)
69
+
70
+ return alert_ids
71
+
72
+
73
+ def parse_triggered(value: str):
74
+ if not value.startswith('[') and not value.startswith('('):
75
+ value = f'-{value.strip()}'
76
+
77
+ return value
78
+
79
+
80
+ ###################################
81
+ # Commands
82
+ ###################################
83
+
84
+
85
+ @banshee_cmd(app=app, help_='Lookup a single Classic Alert', epilog=EPILOG_ALERT_LOOKUP)
86
+ def lookup(
87
+ alert_id: Annotated[
88
+ str, Argument(help='Alert ID to lookup', callback=validate_alert_id, show_default=False)
89
+ ],
90
+ pretty: OPT_PRETTY_PRINT = False,
91
+ ):
92
+ lookup_alert(id_=alert_id, pretty=pretty)
93
+
94
+
95
+ @banshee_cmd(app=app, help_='Search for Classic Alerts', epilog=EPILOG_ALERT_SEARCH)
96
+ def search(
97
+ triggered: Annotated[
98
+ str,
99
+ Option(
100
+ '--triggered',
101
+ '-t',
102
+ callback=parse_triggered,
103
+ help='Filter on triggered time, e.g. 1d; 12h; [2024-08-01, 2024-08-14]; [2024-09-23 12:03:58.000, 2024-09-23 12:03:58.567)', # noqa: E501
104
+ show_default=True,
105
+ ),
106
+ ] = '1d',
107
+ alert_rules: Annotated[
108
+ list[str],
109
+ Option('--rule', '-r', help='Filter by an alert rule name (freetext)', show_default=False),
110
+ ] = None,
111
+ status: Annotated[
112
+ AlertStatus, Option('-s', '--status', help='Filter by alert status', show_default=False)
113
+ ] = None,
114
+ pretty: OPT_PRETTY_PRINT = False,
115
+ ):
116
+ search_alerts(
117
+ triggered=triggered,
118
+ alert_rules=alert_rules,
119
+ status=status,
120
+ pretty=pretty,
121
+ )
122
+
123
+
124
+ @banshee_cmd(app=app, help_='Search Classic Alert rules', epilog=EPILOG_ALERT_RULES_SEARCH)
125
+ def rules(
126
+ freetext: Annotated[str, Argument(help='Freetext to search in alert rules')] = None,
127
+ pretty: OPT_PRETTY_PRINT = False,
128
+ ):
129
+ search_alert_rules(pretty=pretty, freetext=freetext)
130
+
131
+
132
+ @banshee_cmd(app=app, help_='Update a classic alert', epilog=EPILOG_ALERT_UPDATE)
133
+ def update(
134
+ alert_ids: list[str] = Argument( # noqa: B008
135
+ ... if sys.stdin.isatty() else None, # noqa: B008
136
+ show_default=False,
137
+ help='One or more whitespace separated Alert ID',
138
+ ),
139
+ status: Annotated[
140
+ AlertStatus, Option('-s', '--status', help='New alert status', show_default=False)
141
+ ] = None,
142
+ note: Annotated[str, Option('-n', '--note', help='Add a text note', show_default=False)] = None,
143
+ note_append: Annotated[
144
+ bool,
145
+ Option(
146
+ '-A',
147
+ '--append',
148
+ help='Append to the existing text note, instead of overwriting it.',
149
+ show_default=True,
150
+ ),
151
+ ] = False,
152
+ assignee: Annotated[
153
+ str,
154
+ Option(
155
+ '--assignee',
156
+ '-a',
157
+ help='New user to assign the alert(s) to. Accepts uhash or email address of the user, for example: uhash:3aXZxdkM12; analyst@acme.com', # noqa: E501
158
+ show_default=False,
159
+ ),
160
+ ] = None,
161
+ ):
162
+ if alert_ids is None:
163
+ alert_ids = sys.stdin.read()
164
+
165
+ parsed_ids = parse_alert_ids_input(alert_ids)
166
+
167
+ if status is None and note is None and assignee is None:
168
+ raise BadParameter('At least one of --status, --note or --assignee must be privded.')
169
+
170
+ if note_append and note is None:
171
+ raise BadParameter('note argument must be provided when append option is set')
172
+
173
+ update_alerts(
174
+ alert_ids=parsed_ids, status=status, note=note, note_append=note_append, assignee=assignee
175
+ )
@@ -0,0 +1,54 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ from typing import Annotated
15
+
16
+ from typer import Argument, Option, Typer
17
+
18
+ from ..branding import banshee_cmd
19
+ from ..entity_match import EntityType, entity_lookup, entity_search
20
+ from .args import OPT_PRETTY_PRINT
21
+ from .epilogs import EPILOG_ENTITY_LOOKUP, EPILOG_ENTITY_SEARCH
22
+
23
+ CMD_NAME = 'entity'
24
+ CMD_HELP = 'Search and lookup entities'
25
+ CMD_RICH_HELP = 'Entity Match'
26
+
27
+ app = Typer(no_args_is_help=True)
28
+
29
+
30
+ @banshee_cmd(app=app, help_='Lookup an entity by its ID', epilog=EPILOG_ENTITY_LOOKUP)
31
+ def lookup(
32
+ entity_id: str = Argument(show_default=False, help='ID of the entity to lookup'),
33
+ pretty: OPT_PRETTY_PRINT = False,
34
+ ):
35
+ entity_lookup(entity_id=entity_id, pretty=pretty)
36
+
37
+
38
+ @banshee_cmd(
39
+ app=app, help_='Search entities by name and optically by type', epilog=EPILOG_ENTITY_SEARCH
40
+ )
41
+ def search(
42
+ name: str = Argument(show_default=False, help='Name of the entity to search for'),
43
+ type_: Annotated[
44
+ list[EntityType],
45
+ Option(
46
+ '-t', '--type', help='One or more type of the entity to search for', show_default=False
47
+ ),
48
+ ] = None,
49
+ limit: Annotated[
50
+ int, Option('-l', '--limit', help='Limit number of results', min=1, max=100)
51
+ ] = 100,
52
+ pretty: OPT_PRETTY_PRINT = False,
53
+ ):
54
+ entity_search(name=name, type_=type_, limit=limit, pretty=pretty)
@@ -0,0 +1,236 @@
1
+ ##################################### TERMS OF USE ###########################################
2
+ # The following code is provided for demonstration purpose only, and should not be used #
3
+ # without independent verification. Recorded Future makes no representations or warranties, #
4
+ # express, implied, statutory, or otherwise, regarding any aspect of this code or of the #
5
+ # information it may retrieve, and provides it both strictly “as-is” and without assuming #
6
+ # responsibility for any information it may retrieve. Recorded Future shall not be liable #
7
+ # for, and you assume all risk of using, the foregoing. By using this code, Customer #
8
+ # represents that it is solely responsible for having all necessary licenses, permissions, #
9
+ # rights, and/or consents to connect to third party APIs, and that it is solely responsible #
10
+ # for having all necessary licenses, permissions, rights, and/or consents to any data #
11
+ # accessed from any third party API. #
12
+ ##############################################################################################
13
+
14
+ import re
15
+ import sys
16
+ from typing import Annotated, Optional
17
+
18
+ from typer import Argument, BadParameter, Option, Typer
19
+
20
+ from ..branding import banshee_cmd
21
+ from ..indicators import IOCType, lookup_ioc, search_ioc, search_ioc_rules, soar_enrich
22
+ from .args import OPT_PRETTY_PRINT
23
+ from .epilogs import EPILOG_IOC_BULK_LOOKUP, EPILOG_IOC_LOOKUP, EPILOG_IOC_RULES, EPILOG_IOC_SEARCH
24
+
25
+ CMD_NAME = 'ioc'
26
+ CMD_HELP = 'Search and lookup IOCs'
27
+ CMD_RICH_HELP = 'Indicators of Compromise'
28
+
29
+ app = Typer(no_args_is_help=True)
30
+
31
+
32
+ def parse_ioc_input(value: list[str]):
33
+ if not value:
34
+ raise BadParameter('No IOCs supplied')
35
+
36
+ iocs = value
37
+ if isinstance(value, str):
38
+ iocs = [x for x in re.split(r'[\s]+', value) if x]
39
+
40
+ if not len(iocs):
41
+ raise BadParameter('No IOCs provided')
42
+
43
+ return iocs
44
+
45
+
46
+ @banshee_cmd(
47
+ app=app,
48
+ help_=(
49
+ 'Detailed enrichment for one or more IOCs — one API call per indicator. '
50
+ 'Use `--verbosity` to control how many fields are returned, from basic risk score up to '
51
+ 'full intel including links, analyst notes, etc. '
52
+ 'Use this when you need rich context.'
53
+ ),
54
+ epilog=EPILOG_IOC_LOOKUP,
55
+ rich_help_panel='IOC Enrichment',
56
+ )
57
+ def lookup(
58
+ entity_type: Annotated[IOCType, Argument(show_default=False, help='Type of IOC')],
59
+ ioc: list[str] = Argument( # noqa: B008
60
+ ... if sys.stdin.isatty() else None, # noqa: B008
61
+ show_default=False,
62
+ help='One or more whitespace separated IOC',
63
+ ),
64
+ ai_insights: Annotated[
65
+ bool,
66
+ Option(
67
+ '--ai-insights',
68
+ '-a',
69
+ help=(
70
+ 'Enable AI-generated insights from Recorded Future that summarize relevant '
71
+ 'risk rules and key references. Response times may be slightly longer due '
72
+ 'to AI processing.'
73
+ ),
74
+ ),
75
+ ] = False,
76
+ verbosity: Annotated[
77
+ int,
78
+ Option(
79
+ '--verbosity',
80
+ '-v',
81
+ min=1,
82
+ max=5,
83
+ help=(
84
+ 'Controls the amount of data returned in the response. '
85
+ 'Higher verbosity levels include additional fields and details '
86
+ 'in the JSON output. Higher verbosity levels may result in slower '
87
+ 'response times due to increased data retrieval. '
88
+ ),
89
+ ),
90
+ ] = 1,
91
+ pretty: OPT_PRETTY_PRINT = False,
92
+ ):
93
+ if ioc is None:
94
+ ioc = sys.stdin.read()
95
+
96
+ ioc = parse_ioc_input(ioc)
97
+
98
+ lookup_ioc(
99
+ indicators=ioc,
100
+ entity_type=entity_type,
101
+ verbose_level=verbosity,
102
+ pretty=pretty,
103
+ ai_insights=ai_insights,
104
+ )
105
+
106
+
107
+ @banshee_cmd(
108
+ app=app,
109
+ help_=(
110
+ 'Fast bulk enrichment that batches up to 1000 IOCs per API call — '
111
+ 'submit any number of indicators and the command handles the batching automatically. '
112
+ 'Returns a fixed set of fields — risk score and triggered risk rules. '
113
+ 'Use this for high-volume triage. '
114
+ ),
115
+ epilog=EPILOG_IOC_BULK_LOOKUP,
116
+ rich_help_panel='IOC Enrichment',
117
+ )
118
+ def bulk_lookup(
119
+ entity_type: Annotated[IOCType, Argument(show_default=False, help='Type of IOC')],
120
+ ioc: list[str] = Argument( # noqa: B008
121
+ ... if sys.stdin.isatty() else None, # noqa: B008
122
+ show_default=False,
123
+ help='One or more whitespace separated IOC',
124
+ ),
125
+ pretty: OPT_PRETTY_PRINT = False,
126
+ ):
127
+ if ioc is None:
128
+ ioc = sys.stdin.read()
129
+
130
+ ioc = parse_ioc_input(ioc)
131
+
132
+ soar_enrich(indicators=ioc, entity_type=entity_type.value, pretty=pretty)
133
+
134
+
135
+ def parse_risk_score_input(value: str):
136
+ if not value:
137
+ return value
138
+
139
+ if not re.match(r'^[\[\(](\d+|),(\d+|)[\]\)]$', value.strip()):
140
+ raise BadParameter('Invalid risk score format')
141
+
142
+ return value.strip()
143
+
144
+
145
+ @banshee_cmd(
146
+ app=app, help_='Search for IOCs', epilog=EPILOG_IOC_SEARCH, rich_help_panel='IOC Search'
147
+ )
148
+ def search(
149
+ entity_type: Annotated[IOCType, Argument()],
150
+ limit: Annotated[
151
+ Optional[int],
152
+ Option('--limit', '-l', help='Maximum number of IOCs to return', min=1, max=1000),
153
+ ] = 5,
154
+ risk_score: Annotated[
155
+ Optional[str],
156
+ Option(
157
+ '--risk-score',
158
+ '-r',
159
+ help='Filter by risk score range',
160
+ callback=parse_risk_score_input,
161
+ show_default=False,
162
+ ),
163
+ ] = None,
164
+ risk_rule: Annotated[
165
+ Optional[str], Option('--risk-rule', '-R', help='Filter by risk rule', show_default=False)
166
+ ] = None,
167
+ verbosity: Annotated[
168
+ int,
169
+ Option(
170
+ '--verbosity',
171
+ '-v',
172
+ min=1,
173
+ max=5,
174
+ help=(
175
+ 'Controls the amount of data returned in the response. '
176
+ 'Higher verbosity levels include additional fields and details '
177
+ 'in the JSON output. Higher verbosity levels may result in slower '
178
+ 'response times due to increased data retrieval. '
179
+ ),
180
+ ),
181
+ ] = 1,
182
+ pretty: OPT_PRETTY_PRINT = False,
183
+ ):
184
+ search_ioc(
185
+ entity_type=entity_type.value,
186
+ limit=limit,
187
+ risk_score=risk_score,
188
+ risk_rule=risk_rule,
189
+ verbose_level=verbosity,
190
+ pretty=pretty,
191
+ )
192
+
193
+
194
+ @banshee_cmd(
195
+ app=app, help_='Search for IOC Rules', epilog=EPILOG_IOC_RULES, rich_help_panel='IOC Rules'
196
+ )
197
+ def rules(
198
+ entity_type: Annotated[IOCType, Argument(show_default=False, help='Type of IOC')],
199
+ freetext: Annotated[
200
+ str,
201
+ Option(
202
+ '-F',
203
+ '--freetext',
204
+ show_default=False,
205
+ help='Free text search to filter rules by name/description',
206
+ ),
207
+ ] = None,
208
+ mitre_code: Annotated[
209
+ str,
210
+ Option(
211
+ '-M',
212
+ '--mitre-code',
213
+ show_default=False,
214
+ help='Filter by MITRE ATT&CK code',
215
+ ),
216
+ ] = None,
217
+ criticality: Annotated[
218
+ int,
219
+ Option(
220
+ '-C',
221
+ '--criticality',
222
+ show_default=False,
223
+ min=0,
224
+ max=5,
225
+ help='Filter by criticality. Higher the value, higher the criticality',
226
+ ),
227
+ ] = None,
228
+ pretty: OPT_PRETTY_PRINT = False,
229
+ ):
230
+ search_ioc_rules(
231
+ entity_type=entity_type.value,
232
+ freetext=freetext,
233
+ mitre_code=mitre_code,
234
+ criticality=criticality,
235
+ pretty=pretty,
236
+ )