Skip to content

Commit 2c13006

Browse files
authored
feat: Add CLI and API support for ASM saved queries (#598)
1 parent ef145f1 commit 2c13006

27 files changed

+1267
-19
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ An easy-to-use and lightweight API wrapper for Censys APIs ([censys.io](https://
2020
- [Command-line interface](https://censys-python.readthedocs.io/en/stable/cli.html)
2121

2222
<!-- markdownlint-disable MD033 -->
23+
2324
<a href="https://asciinema.org/a/500416" target="_blank"><img src="https://asciinema.org/a/500416.svg" width="600"/></a>
25+
2426
<!-- markdownlint-enable MD033 -->
2527

2628
## Getting Started
@@ -121,4 +123,4 @@ poetry run pytest --cov-report html
121123

122124
This software is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
123125

124-
- Copyright (C) 2023 Censys, Inc.
126+
- Copyright (C) 2024 Censys, Inc.

Diff for: censys/asm/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .inventory import InventorySearch
1515
from .logbook import Events, Logbook
1616
from .risks import Risks
17+
from .saved_queries import SavedQueries
1718
from .seeds import Seeds
1819

1920
__all__ = [
@@ -28,6 +29,7 @@
2829
"InventorySearch",
2930
"Logbook",
3031
"Risks",
32+
"SavedQueries",
3133
"Seeds",
3234
"SubdomainsAssets",
3335
"WebEntitiesAssets",

Diff for: censys/asm/client.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .inventory import InventorySearch
1515
from .logbook import Logbook
1616
from .risks import Risks
17+
from .saved_queries import SavedQueries
1718
from .seeds import Seeds
1819

1920

@@ -40,3 +41,4 @@ def __init__(self, api_key: Optional[str] = None, **kwargs):
4041
self.object_storages = ObjectStoragesAssets(api_key, **kwargs)
4142
self.web_entities = WebEntitiesAssets(api_key, **kwargs)
4243
self.beta = Beta(api_key, **kwargs)
44+
self.saved_queries = SavedQueries(api_key, **kwargs)

Diff for: censys/asm/saved_queries.py

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Interact with the Censys Saved Queries API."""
2+
from typing import Optional
3+
4+
from .api import CensysAsmAPI
5+
6+
7+
class SavedQueries(CensysAsmAPI):
8+
"""Saved Queries API class."""
9+
10+
base_path = "/inventory/v1/saved-query"
11+
12+
def get_saved_queries(
13+
self,
14+
query_name_prefix: Optional[str] = None,
15+
page_size: Optional[int] = None,
16+
page: Optional[int] = None,
17+
filter_term: Optional[str] = None,
18+
) -> dict:
19+
"""Get saved queries.
20+
21+
Args:
22+
query_name_prefix (str, optional): Prefix for the saved query name.
23+
page_size (int, optional): Number of results to return. Defaults to 50.
24+
page (int, optional): Page number to begin at when searching. Defaults to 1.
25+
filter_term (str, optional): Term used to filter the list of saved query names and the saved queries.
26+
27+
Returns:
28+
dict: Saved queries results.
29+
"""
30+
if page_size is None:
31+
page_size = 50
32+
if page is None:
33+
page = 1
34+
args: dict = {
35+
"pageSize": page_size,
36+
"page": page,
37+
}
38+
39+
if query_name_prefix:
40+
args["queryNamePrefix"] = query_name_prefix
41+
if filter_term:
42+
args["filterTerm"] = filter_term
43+
44+
return self._get(self.base_path, args=args)
45+
46+
def add_saved_query(
47+
self,
48+
query: str,
49+
query_name: str,
50+
) -> dict:
51+
"""Add a new saved query to the ASM platform.
52+
53+
Args:
54+
query (str): Query string.
55+
query_name (str): Saved query name.
56+
57+
Returns:
58+
dict: Added saved query results.
59+
"""
60+
body = {
61+
"query": query,
62+
"queryName": query_name,
63+
}
64+
65+
return self._post(self.base_path, data=body)
66+
67+
def get_saved_query_by_id(
68+
self,
69+
query_id: str,
70+
) -> dict:
71+
"""Get saved query by query ID.
72+
73+
Args:
74+
query_id (str): The saved query's ID.
75+
76+
Returns:
77+
dict: Saved query result.
78+
"""
79+
return self._get(f"{self.base_path}/{query_id}")
80+
81+
def edit_saved_query_by_id(
82+
self,
83+
query_id: str,
84+
query: str,
85+
query_name: str,
86+
) -> dict:
87+
"""Edit an existing saved query by query ID.
88+
89+
Args:
90+
query_id (str): The saved query's ID.
91+
query (str): New query string.
92+
query_name (str): New saved query name.
93+
94+
Returns:
95+
dict: Edited saved query result.
96+
"""
97+
body = {
98+
"query": query,
99+
"queryName": query_name,
100+
}
101+
102+
return self._put(f"{self.base_path}/{query_id}", data=body)
103+
104+
def delete_saved_query_by_id(
105+
self,
106+
query_id: str,
107+
) -> dict:
108+
"""Delete saved query by query ID.
109+
110+
Args:
111+
query_id (str): The saved query's ID.
112+
113+
Returns:
114+
dict: Delete results.
115+
"""
116+
return self._delete(f"{self.base_path}/{query_id}")

Diff for: censys/cli/commands/account.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def cli_account(args: argparse.Namespace): # pragma: no cover
3232
quota = account["quota"]
3333
table.add_row(
3434
"Query Quota",
35-
f"{quota['used']} / {quota['allowance']} ({quota['used']/quota['allowance'] * 100 :.2f}%)",
35+
f"{quota['used']} / {quota['allowance']} ({quota['used']/quota['allowance'] * 100 :.2f}%)", # noqa
3636
)
3737
table.add_row("Quota Resets At", quota["resets_at"])
3838
console.print(table)

Diff for: censys/cli/commands/asm.py

+207
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from rich.progress import Progress, TaskID
1212
from rich.prompt import Confirm, Prompt
1313

14+
from censys.asm.saved_queries import SavedQueries
1415
from censys.asm.seeds import SEED_TYPES, Seeds
1516
from censys.cli.utils import console
1617
from censys.common.config import DEFAULT, get_config, write_config
1718
from censys.common.exceptions import (
19+
CensysAsmException,
1820
CensysSeedNotFoundException,
1921
CensysUnauthorizedException,
2022
)
@@ -437,6 +439,97 @@ def add_seed_arguments(parser: argparse._SubParsersAction, is_delete=False) -> N
437439
)
438440

439441

442+
def cli_list_saved_queries(args: argparse.Namespace):
443+
"""List saved queries subcommand.
444+
445+
Args:
446+
args (Namespace): Argparse Namespace.
447+
"""
448+
s = SavedQueries(args.api_key)
449+
try:
450+
res = s.get_saved_queries(
451+
args.query_name_prefix, args.page_size, args.page, args.filter_term
452+
)
453+
454+
if args.csv:
455+
console.print("queryId,queryName,query,createdAt")
456+
for query in res["results"]:
457+
console.print(
458+
f"{query['queryId']},{query['queryName']},{query['query']},{query['createdAt']}" # noqa: E231
459+
)
460+
else:
461+
console.print_json(json.dumps(res))
462+
except KeyError:
463+
console.print("Failed to get saved queries.")
464+
sys.exit(1)
465+
466+
467+
def cli_add_saved_query(args: argparse.Namespace):
468+
"""Add saved query subcommand.
469+
470+
Args:
471+
args (Namespace): Argparse Namespace.
472+
"""
473+
s = SavedQueries(args.api_key)
474+
res = s.add_saved_query(args.query, args.query_name)
475+
try:
476+
console.print(
477+
f"Added saved query.\nQuery name: {res['result']['queryName']}\nQuery: {res['result']['query']}\nQuery ID: {res['result']['queryId']}\nCreated at: {res['result']['createdAt']}"
478+
)
479+
except (KeyError, CensysAsmException):
480+
console.print("Failed to add saved query.")
481+
sys.exit(1)
482+
483+
484+
def cli_get_saved_query_by_id(args: argparse.Namespace):
485+
"""Get saved query by id subcommand.
486+
487+
Args:
488+
args (Namespace): Argparse Namespace.
489+
"""
490+
s = SavedQueries(args.api_key)
491+
res = s.get_saved_query_by_id(args.query_id)
492+
try:
493+
console.print(
494+
f"Query name: {res['result']['queryName']}\nQuery: {res['result']['query']}\nQuery ID: {res['result']['queryId']}\nCreated at: {res['result']['createdAt']}"
495+
)
496+
except (KeyError, CensysAsmException):
497+
console.print("Failed to get saved query.")
498+
sys.exit(1)
499+
500+
501+
def cli_edit_saved_query_by_id(args: argparse.Namespace):
502+
"""Edit saved query by id subcommand.
503+
504+
Args:
505+
args (Namespace): Argparse Namespace.
506+
"""
507+
s = SavedQueries(args.api_key)
508+
res = s.edit_saved_query_by_id(args.query_id, args.query, args.query_name)
509+
try:
510+
console.print(
511+
f"Edited saved query.\nQuery name: {res['result']['queryName']}\nQuery: {res['result']['query']}\nQuery ID: {res['result']['queryId']}\nCreated at: {res['result']['createdAt']}"
512+
)
513+
except (KeyError, CensysAsmException):
514+
console.print("Failed to edit saved query.")
515+
sys.exit(1)
516+
517+
518+
def cli_delete_saved_query_by_id(args: argparse.Namespace):
519+
"""Delete saved query by id subcommand.
520+
521+
Args:
522+
args (Namespace): Argparse Namespace.
523+
"""
524+
s = SavedQueries(args.api_key)
525+
res = s.delete_saved_query_by_id(args.query_id)
526+
if res == {}:
527+
console.print(f"Deleted saved query with ID {args.query_id}.")
528+
else:
529+
console.print("Failed to delete saved query.")
530+
sys.exit(1)
531+
532+
440533
def include(parent_parser: argparse._SubParsersAction, parents: dict):
441534
"""Include this subcommand into the parent parser.
442535
@@ -568,3 +661,117 @@ def add_verbose(parser):
568661
)
569662
add_verbose(list_parser)
570663
list_parser.set_defaults(func=cli_list_seeds)
664+
665+
list_saved_queries_parser = asm_subparser.add_parser(
666+
"list-saved-queries",
667+
description="List all ASM saved queries, optionally filtered by query name prefix and filter term",
668+
help="list saved queries",
669+
parents=[parents["asm_auth"]],
670+
)
671+
list_saved_queries_parser.add_argument(
672+
"--query-name-prefix",
673+
help="Prefix for the saved query name to filter by",
674+
type=str,
675+
default="",
676+
)
677+
list_saved_queries_parser.add_argument(
678+
"--filter-term",
679+
help="Term used to filter the list of saved query names and the saved queries",
680+
type=str,
681+
default="",
682+
)
683+
list_saved_queries_parser.add_argument(
684+
"--page-size",
685+
help="Number of results to return. Defaults to 50.",
686+
type=int,
687+
default=50,
688+
)
689+
list_saved_queries_parser.add_argument(
690+
"--page",
691+
help="Page number to begin at when searching. Defaults to 1.",
692+
type=int,
693+
default=1,
694+
)
695+
list_saved_queries_parser.add_argument(
696+
"--csv", help="output in CSV format (otherwise JSON)", action="store_true"
697+
)
698+
add_verbose(list_saved_queries_parser)
699+
list_saved_queries_parser.set_defaults(func=cli_list_saved_queries)
700+
701+
add_saved_query_parser = asm_subparser.add_parser(
702+
"add-saved-query",
703+
description="Add a saved query to ASM",
704+
help="add saved query",
705+
parents=[parents["asm_auth"]],
706+
)
707+
add_saved_query_parser.add_argument(
708+
"--query-name",
709+
help="Name of the saved query",
710+
type=str,
711+
required=True,
712+
)
713+
add_saved_query_parser.add_argument(
714+
"--query",
715+
help="Query string",
716+
type=str,
717+
required=True,
718+
)
719+
add_verbose(add_saved_query_parser)
720+
add_saved_query_parser.set_defaults(func=cli_add_saved_query)
721+
722+
get_saved_query_by_id_parser = asm_subparser.add_parser(
723+
"get-saved-query-by-id",
724+
description="Get a saved query by ID",
725+
help="get saved query by ID",
726+
parents=[parents["asm_auth"]],
727+
)
728+
get_saved_query_by_id_parser.add_argument(
729+
"--query-id",
730+
help="ID of the saved query",
731+
type=str,
732+
required=True,
733+
)
734+
add_verbose(get_saved_query_by_id_parser)
735+
get_saved_query_by_id_parser.set_defaults(func=cli_get_saved_query_by_id)
736+
737+
edit_saved_query_by_id_parser = asm_subparser.add_parser(
738+
"edit-saved-query-by-id",
739+
description="Edit a saved query by ID",
740+
help="edit saved query by ID",
741+
parents=[parents["asm_auth"]],
742+
)
743+
edit_saved_query_by_id_parser.add_argument(
744+
"--query-id",
745+
help="ID of the saved query",
746+
type=str,
747+
required=True,
748+
)
749+
edit_saved_query_by_id_parser.add_argument(
750+
"--query-name",
751+
help="Name of the saved query",
752+
type=str,
753+
required=True,
754+
)
755+
edit_saved_query_by_id_parser.add_argument(
756+
"--query",
757+
help="Query string",
758+
type=str,
759+
required=True,
760+
)
761+
add_verbose(edit_saved_query_by_id_parser)
762+
edit_saved_query_by_id_parser.set_defaults(func=cli_edit_saved_query_by_id)
763+
764+
delete_saved_query_by_id_parser = asm_subparser.add_parser(
765+
"delete-saved-query-by-id",
766+
description="Delete a saved query by ID",
767+
help="delete saved query by ID",
768+
parents=[parents["asm_auth"]],
769+
)
770+
delete_saved_query_by_id_parser.add_argument(
771+
"--query-id",
772+
help="ID of the saved query",
773+
type=str,
774+
required=True,
775+
)
776+
add_verbose(delete_saved_query_by_id_parser)
777+
delete_saved_query_by_id_parser.set_defaults(func=cli_delete_saved_query_by_id)

0 commit comments

Comments
 (0)