Skip to content

Commit 5178af0

Browse files
authored
Merge pull request #266 from censys/grace-murphy/censys-search-csv-fix
Edit output format options for Search/View CLI
2 parents 8294fef + 89646c9 commit 5178af0

File tree

5 files changed

+85
-41
lines changed

5 files changed

+85
-41
lines changed

censys/cli/commands/search.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ def cli_search(args: argparse.Namespace):
7171
if args.max_records:
7272
search_args["max_records"] = args.max_records
7373

74+
if args.output:
75+
if args.output.endswith(".csv"):
76+
write_args["file_format"] = "csv"
77+
else:
78+
write_args["file_format"] = "json"
79+
else:
80+
write_args["file_format"] = "screen"
81+
7482
fields: List[str] = []
7583
if args.fields:
7684
if args.overwrite:
@@ -91,12 +99,17 @@ def cli_search(args: argparse.Namespace):
9199
with err_console.status("Searching"):
92100
results = list(index.search(args.query, **search_args))
93101
elif index_type in V2_INDEXES:
94-
if args.format == "csv":
102+
if args.format == "csv" or (args.output and not args.output.endswith(".json")):
95103
raise CensysCLIException(
96-
f"CSV output is not supported for the {index_type} index."
104+
"JSON is the only valid file format for Search 2.0 responses."
97105
)
98106
index = getattr(c.v2, index_type)
99107

108+
if args.output:
109+
write_args["file_format"] = "json"
110+
else:
111+
write_args["file_format"] = "screen"
112+
100113
if args.pages:
101114
search_args["pages"] = args.pages
102115

@@ -162,7 +175,7 @@ def include(parent_parser: argparse._SubParsersAction, parents: dict):
162175
default="screen",
163176
choices=["screen", "json", "csv"],
164177
metavar="screen|json|csv",
165-
help="format of output (csv is only supported for the certificates index)",
178+
help=argparse.SUPPRESS,
166179
)
167180
search_parser.add_argument(
168181
"-o",

censys/cli/commands/view.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ def cli_view(args: argparse.Namespace):
3636
write_args = {
3737
"file_format": "json" if args.output else "screen",
3838
"file_path": args.output,
39-
"base_name": f"censys-view-{args.document_id}",
4039
}
4140

4241
if args.at_time:

censys/cli/utils.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ def _write_screen(search_results: Results): # pragma: no cover
8686

8787
def write_file(
8888
results_list: Results,
89-
file_format: str = "screen",
89+
file_format: Optional[str] = None,
9090
file_path: Optional[str] = None,
91-
base_name: str = "censys-query-output",
9291
csv_fields: List[str] = [],
9392
):
9493
"""Maps formats and writes results.
@@ -97,17 +96,13 @@ def write_file(
9796
results_list (Results): A list of results from the API query.
9897
file_format (str): Optional; The format of the output.
9998
file_path (str): Optional; A path to write results to.
100-
base_name (str): Optional; The base name of the output file.
10199
csv_fields (List[str]): Optional; A list of fields to write to CSV.
102100
"""
103101
if file_format and isinstance(file_format, str):
104102
file_format = file_format.lower()
105103

106104
if not file_path:
107-
# This method just creates some dynamic file names
108-
file_path = ".".join([base_name, file_format])
109-
elif file_path.endswith(".json"):
110-
file_format = "json"
105+
file_path = "temp-out.json"
111106

112107
if file_format == "json":
113108
_write_json(file_path, results_list)

docs/usage-cli.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ Below we show an example of Viewing a host from the CLI.
4444

4545
censys view 8.8.8.8
4646

47-
You can save results to a file using the ``-f`` and ``-o`` arguments.
47+
You can save results to a file using the ``-o`` argument.
4848

4949
.. prompt:: bash
5050

51-
censys view 8.8.8.8 -f json -o google.json
51+
censys view 8.8.8.8 -o google.json
5252

5353
We can then parse this json with something like ``jq``.
5454

tests/cli/test_search.py

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def test_no_creds(self, mock_file):
5555
"certs",
5656
"--fields",
5757
"parsed.issuer.country",
58-
"--format",
59-
"json",
58+
"--output",
59+
"censys-certs.json",
6060
]
6161
+ CensysTestCase.cli_args,
6262
)
@@ -77,7 +77,7 @@ def test_write_json(self):
7777

7878
json_path = cli_response.replace(WROTE_PREFIX, "").strip()
7979
assert json_path.endswith(".json")
80-
assert "censys-query-output." in json_path
80+
assert "censys-certs." in json_path
8181

8282
with open(json_path) as json_file:
8383
json_response = json.load(json_file)
@@ -98,8 +98,8 @@ def test_write_json(self):
9898
"certs",
9999
"--fields",
100100
"protocols",
101-
"--format",
102-
"csv",
101+
"--output",
102+
"censys-certs.csv",
103103
]
104104
+ CensysTestCase.cli_args,
105105
)
@@ -120,7 +120,7 @@ def test_write_csv(self):
120120

121121
csv_path = cli_response.replace(WROTE_PREFIX, "").strip()
122122
assert csv_path.endswith(".csv")
123-
assert "censys-query-output." in csv_path
123+
assert "censys-certs." in csv_path
124124

125125
with open(csv_path) as csv_file:
126126
csv_reader = csv.reader(csv_file)
@@ -141,35 +141,38 @@ def test_write_csv(self):
141141
"certs",
142142
"--fields",
143143
"parsed.issuer.country",
144-
"--format",
145-
"json",
146144
"--output",
147-
"censys-certs.json",
145+
"censys-certs.html",
148146
]
149147
+ CensysTestCase.cli_args,
150148
)
151-
def test_write_output_path(self):
149+
def test_write_invalid_output_path(self):
152150
self.responses.add_callback(
153151
responses.POST,
154152
V1_URL + "/search/certificates",
155153
callback=search_callback,
156154
content_type="application/json",
157155
)
158156

159-
output_path = "censys-certs.json"
157+
temp_stdout = StringIO()
158+
with contextlib.redirect_stdout(temp_stdout):
159+
cli_main()
160160

161-
cli_main()
161+
cli_response = temp_stdout.getvalue().strip()
162+
assert cli_response.startswith(WROTE_PREFIX)
162163

163-
assert os.path.isfile(output_path)
164+
json_path = cli_response.replace(WROTE_PREFIX, "").strip()
165+
assert json_path.endswith(".html")
166+
assert "censys-certs." in json_path
164167

165-
with open(output_path) as json_file:
168+
with open(json_path) as json_file:
166169
json_response = json.load(json_file)
167170

168171
assert len(json_response) >= 1
169172
assert "parsed.issuer.country" in json_response[0]
170173

171174
# Cleanup
172-
os.remove(output_path)
175+
os.remove(json_path)
173176

174177
@patch(
175178
"argparse._sys.argv",
@@ -181,8 +184,6 @@ def test_write_output_path(self):
181184
"certs",
182185
"--fields",
183186
"443.https.get.headers.server",
184-
"--format",
185-
"screen",
186187
]
187188
+ CensysTestCase.cli_args,
188189
)
@@ -220,8 +221,6 @@ def test_write_screen(self):
220221
"443.https.tls.cipher_suite.name",
221222
"443.https.get.title",
222223
"443.https.get.headers.server",
223-
"--format",
224-
"screen",
225224
]
226225
+ CensysTestCase.cli_args,
227226
)
@@ -300,8 +299,6 @@ def test_field_max(self):
300299
"parsed.names: censys.io",
301300
"--index-type",
302301
"certs",
303-
"--format",
304-
"screen",
305302
"--max-records",
306303
"2",
307304
]
@@ -331,8 +328,6 @@ def test_max_records(self):
331328
"service.service_name: HTTP",
332329
"--index-type",
333330
"hosts",
334-
"--format",
335-
"screen",
336331
"--pages",
337332
"1",
338333
]
@@ -363,8 +358,6 @@ def test_write_screen_v2(self):
363358
"service.service_name: HTTP",
364359
"--index-type",
365360
"hosts",
366-
"--format",
367-
"screen",
368361
"--pages",
369362
"1",
370363
"--virtual-hosts",
@@ -397,17 +390,61 @@ def test_search_virtual_hosts(self):
397390
"service.service_name: HTTP",
398391
"--index-type",
399392
"hosts",
400-
"--format",
401-
"csv",
402393
"--pages",
403394
"1",
395+
"--output",
396+
"censys-hosts.json",
397+
]
398+
+ CensysTestCase.cli_args,
399+
)
400+
def test_write_json_v2(self):
401+
self.responses.add(
402+
responses.GET,
403+
V2_URL
404+
+ "/hosts/search?q=service.service_name: HTTP&per_page=100&virtual_hosts=EXCLUDE",
405+
status=200,
406+
json=SEARCH_HOSTS_JSON,
407+
)
408+
409+
temp_stdout = StringIO()
410+
with contextlib.redirect_stdout(temp_stdout):
411+
cli_main()
412+
413+
cli_response = temp_stdout.getvalue().strip()
414+
assert cli_response.startswith(WROTE_PREFIX)
415+
416+
json_path = cli_response.replace(WROTE_PREFIX, "").strip()
417+
assert json_path.endswith(".json")
418+
assert "censys-hosts." in json_path
419+
420+
with open(json_path) as json_file:
421+
json_response = json.load(json_file)
422+
423+
assert len(json_response) >= 1
424+
assert json_response == SEARCH_HOSTS_JSON["result"]["hits"]
425+
426+
# Cleanup
427+
os.remove(json_path)
428+
429+
@patch(
430+
"argparse._sys.argv",
431+
[
432+
"censys",
433+
"search",
434+
"service.service_name: HTTP",
435+
"--index-type",
436+
"hosts",
437+
"--pages",
438+
"1",
439+
"--output",
440+
"censys-hosts.csv",
404441
]
405442
+ CensysTestCase.cli_args,
406443
)
407444
def test_write_csv_v2(self):
408445
with pytest.raises(
409446
CensysCLIException,
410-
match="CSV output is not supported for the hosts index.",
447+
match="JSON is the only valid file format for Search 2.0 responses.",
411448
):
412449
cli_main()
413450

0 commit comments

Comments
 (0)