Skip to content

Commit 36693fc

Browse files
Merge pull request #159 from censys/adh/comments-and-tags
feat(api): Add Tagging and Comments
2 parents a9cac08 + 841716c commit 36693fc

File tree

11 files changed

+705
-24
lines changed

11 files changed

+705
-24
lines changed

.github/workflows/python-ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ jobs:
3535
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
3636

3737
- name: Install dependencies
38-
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
3938
run: |
4039
poetry install
4140

censys/common/base.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
import warnings
55
from functools import wraps
6-
from typing import Any, Callable, List, Optional, Type
6+
from typing import Any, Callable, List, Optional, Protocol, Type
77

88
import backoff
99
import requests
@@ -43,6 +43,11 @@ def _impl():
4343
return _wrapper
4444

4545

46+
class RequestsMethod(Protocol): # noqa: D101
47+
def __call__(self, path: str, **kwargs) -> requests.Response: # noqa: D102
48+
... # pragma: no cover
49+
50+
4651
class CensysAPIBase:
4752
"""This is the base class for API queries."""
4853

@@ -126,7 +131,7 @@ def _get_exception_class(_: Response) -> Type[CensysAPIException]:
126131
@_backoff_wrapper
127132
def _make_call(
128133
self,
129-
method: Callable,
134+
method: RequestsMethod,
130135
endpoint: str,
131136
args: Optional[dict] = None,
132137
data: Optional[Any] = None,
@@ -137,7 +142,7 @@ def _make_call(
137142
and decoding the responses.
138143
139144
Args:
140-
method (Callable): Method to send HTTP request.
145+
method (RequestsMethod): Method to send HTTP request.
141146
endpoint (str): The path of API endpoint.
142147
args (dict): Optional; URL args that are mapped to params.
143148
data (Any): Optional; JSON data to serialize with request.
@@ -164,15 +169,18 @@ def _make_call(
164169

165170
res = method(url, **request_kwargs)
166171

167-
if res.status_code == 200:
172+
if res.status_code in range(200, 300):
168173
# Check for a returned json body
169174
try:
170175
json_data = res.json()
171176
if "error" not in json_data:
172177
return json_data
173178
# Successful request returned no json body in response
174179
except ValueError:
175-
return {}
180+
return {
181+
"code": res.status_code,
182+
"status": res.reason,
183+
}
176184

177185
try:
178186
json_data = res.json()

censys/search/v2/api.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Base for interacting with the Censys Search API."""
22
import os
33
from concurrent.futures import ThreadPoolExecutor, as_completed
4-
from typing import Dict, Iterable, Iterator, List, Optional, Type
4+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Type
55

66
from requests.models import Response
77

@@ -74,6 +74,7 @@ def __init__(
7474
self.search_path = f"/{self.INDEX_NAME}/search"
7575
self.aggregate_path = f"/{self.INDEX_NAME}/aggregate"
7676
self.metadata_path = f"/metadata/{self.INDEX_NAME}"
77+
self.tags_path = "/tags"
7778

7879
# Set up the v1 API
7980
v1_kwargs = kwargs.copy()
@@ -286,3 +287,141 @@ def metadata(self) -> dict:
286287
dict: The result set returned.
287288
"""
288289
return self._get(self.metadata_path)["result"]
290+
291+
# Comments
292+
293+
def get_comments(self, document_id: str) -> List[dict]:
294+
"""Get comments for a document.
295+
296+
Args:
297+
document_id (str): The ID of the document you are requesting.
298+
299+
Returns:
300+
List[dict]: The list of comments.
301+
"""
302+
return self._get(self.view_path + document_id + "/comments")["result"][
303+
"comments"
304+
]
305+
306+
def add_comment(self, document_id: str, contents: str) -> dict:
307+
"""Add comment to a document.
308+
309+
Args:
310+
document_id (str): The ID of the document you are requesting.
311+
contents (str): The contents of the comment.
312+
313+
Returns:
314+
dict: The result set returned.
315+
"""
316+
return self._post(
317+
self.view_path + document_id + "/comments", data={"contents": contents}
318+
)["result"]
319+
320+
# Tags
321+
322+
def list_all_tags(self) -> List[dict]:
323+
"""List all tags.
324+
325+
Returns:
326+
List[dict]: The list of tags.
327+
"""
328+
return self._get(self.tags_path)["result"]["tags"]
329+
330+
def create_tag(self, name: str, color: Optional[str] = None) -> dict:
331+
"""Create a tag.
332+
333+
Args:
334+
name (str): The name of the tag.
335+
color (str): Optional; The color of the tag.
336+
337+
Returns:
338+
dict: The result set returned.
339+
"""
340+
tag_def: Dict[str, Any] = {"name": name}
341+
if color:
342+
tag_def["metadata"] = {"color": color}
343+
return self._post(self.tags_path, data=tag_def)["result"]
344+
345+
def get_tag(self, tag_id: str) -> dict:
346+
"""Get a tag.
347+
348+
Args:
349+
tag_id (str): The ID of the tag.
350+
351+
Returns:
352+
dict: The result set returned.
353+
"""
354+
return self._get(self.tags_path + "/" + tag_id)["result"]
355+
356+
def update_tag(self, tag_id: str, name: str, color: Optional[str] = None) -> dict:
357+
"""Update a tag.
358+
359+
Args:
360+
tag_id (str): The ID of the tag.
361+
name (str): The name of the tag.
362+
color (str): The color of the tag.
363+
364+
Returns:
365+
dict: The result set returned.
366+
"""
367+
tag_def: Dict[str, Any] = {"name": name}
368+
if color:
369+
tag_def["metadata"] = {"color": color}
370+
return self._put(
371+
self.tags_path + "/" + tag_id,
372+
data=tag_def,
373+
)["result"]
374+
375+
def delete_tag(self, tag_id: str):
376+
"""Delete a tag.
377+
378+
Args:
379+
tag_id (str): The ID of the tag.
380+
"""
381+
self._delete(self.tags_path + "/" + tag_id)
382+
383+
def _list_documents_with_tag(
384+
self, tag_id: str, endpoint: str, keyword: str
385+
) -> List[dict]:
386+
"""List documents by tag.
387+
388+
Args:
389+
tag_id (str): The ID of the tag.
390+
endpoint (str): The endpoint to be called.
391+
keyword (str): The keyword to be used in the endpoint.
392+
393+
Returns:
394+
List[dict]: The list of documents.
395+
"""
396+
return self._get(self.tags_path + "/" + tag_id + "/" + endpoint)["result"][
397+
keyword
398+
]
399+
400+
def list_tags_on_document(self, document_id: str) -> List[dict]:
401+
"""List tags on a document.
402+
403+
Args:
404+
document_id (str): The ID of the document.
405+
406+
Returns:
407+
List[dict]: The list of tags.
408+
"""
409+
return self._get(self.view_path + document_id + "/tags")["result"]["tags"]
410+
411+
def add_tag_to_document(self, document_id: str, tag_id: str):
412+
"""Add a tag to a document.
413+
414+
Args:
415+
document_id (str): The ID of the document.
416+
tag_id (str): The ID of the tag.
417+
"""
418+
self._put(self.view_path + document_id + "/tags/" + tag_id)
419+
420+
def remove_tag_from_document(self, document_id: str, tag_id: str):
421+
"""Remove a tag from a document.
422+
423+
Args:
424+
document_id (str): The ID of the document.
425+
tag_id (str): The ID of the tag.
426+
"""
427+
self._delete(self.view_path + document_id + "/tags/" + tag_id)

censys/search/v2/certs.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Interact with the Censys Search Host API."""
1+
"""Interact with the Censys Search Cert API."""
22
from typing import List, Optional, Tuple
33

44
from .api import CensysSearchAPIv2
@@ -16,14 +16,19 @@ class CensysCerts(CensysSearchAPIv2):
1616
Search for hosts by sha256fp.
1717
1818
>>> c.get_hosts_by_cert("fb444eb8e68437bae06232b9f5091bccff62a768ca09e92eb5c9c2cf9d17c426")
19-
[
19+
(
20+
[
21+
{
22+
"ip": "string",
23+
"name": "string",
24+
"observed_at": "2021-08-02T14:56:38.711Z",
25+
"first_observed_at": "2021-08-02T14:56:38.711Z",
26+
}
27+
],
2028
{
21-
"ip": "string",
22-
"name": "string",
23-
"observed_at": "2021-08-02T14:56:38.711Z",
24-
"first_observed_at": "2021-08-02T14:56:38.711Z",
25-
}
26-
]
29+
"next": "nextCursorToken",
30+
},
31+
)
2732
"""
2833

2934
INDEX_NAME = "certificates"
@@ -58,3 +63,15 @@ def get_hosts_by_cert(
5863
args = {"cursor": cursor}
5964
result = self._get(self.view_path + sha256fp + "/hosts", args)["result"]
6065
return result["hosts"], result["links"]
66+
67+
def list_certs_with_tag(self, tag_id: str) -> List[str]:
68+
"""Returns a list of certs which are tagged with the specified tag.
69+
70+
Args:
71+
tag_id (str): The ID of the tag.
72+
73+
Returns:
74+
List[str]: A list of certificate SHA 256 fingerprints.
75+
"""
76+
certs = self._list_documents_with_tag(tag_id, "certificates", "certs")
77+
return [cert["fingerprint"] for cert in certs]

censys/search/v2/hosts.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CensysHosts(CensysSearchAPIv2):
3131
...
3232
]
3333
34-
View specific host.
34+
Fetch a specific host and its services
3535
3636
>>> h.view("1.0.0.0")
3737
{
@@ -57,6 +57,16 @@ class CensysHosts(CensysSearchAPIv2):
5757
'query': 'service.service_name: HTTP',
5858
'total': 172588754
5959
}
60+
61+
Fetch a list of host names for the specified IP address.
62+
63+
>>> h.view_host_names("1.1.1.1")
64+
['one.one.one.one']
65+
66+
Fetch a list of events for the specified IP address.
67+
68+
>>> h.view_host_events("1.1.1.1")
69+
[{'timestamp': '2019-01-01T00:00:00.000Z'}]
6070
"""
6171

6272
INDEX_NAME = "hosts"
@@ -108,3 +118,15 @@ def view_host_events(
108118
return self._get(f"/experimental/{self.INDEX_NAME}/{ip_address}/events", args)[
109119
"result"
110120
]["events"]
121+
122+
def list_hosts_with_tag(self, tag_id: str) -> List[str]:
123+
"""Returns a list of hosts which are tagged with the specified tag.
124+
125+
Args:
126+
tag_id (str): The ID of the tag.
127+
128+
Returns:
129+
List[str]: A list of host IP addresses.
130+
"""
131+
hosts = self._list_documents_with_tag(tag_id, "hosts", "hosts")
132+
return [host["ip"] for host in hosts]

docs/usage-v1.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Below we show an example using the :attr:`CensysIPv4 <censys.search.v1.CensysIPv
132132
``bulk``
133133
--------
134134

135-
**Please note this method is only available only for the certificate index**
135+
**Please note this method is only available only for the CensysCertificates index**
136136

137137
Below we show an example using the :attr:`CensysCertificates <censys.search.v1.CensysCertificates>` index.
138138

0 commit comments

Comments
 (0)