cartography 0.103.0rc1__py3-none-any.whl → 0.104.0rc2__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 (73) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +97 -1
  3. cartography/config.py +28 -0
  4. cartography/intel/anthropic/__init__.py +62 -0
  5. cartography/intel/anthropic/apikeys.py +72 -0
  6. cartography/intel/anthropic/users.py +75 -0
  7. cartography/intel/anthropic/util.py +51 -0
  8. cartography/intel/anthropic/workspaces.py +95 -0
  9. cartography/intel/aws/cloudwatch.py +93 -0
  10. cartography/intel/aws/ec2/load_balancer_v2s.py +4 -1
  11. cartography/intel/aws/efs.py +93 -0
  12. cartography/intel/aws/resources.py +4 -0
  13. cartography/intel/aws/secretsmanager.py +136 -3
  14. cartography/intel/aws/ssm.py +71 -0
  15. cartography/intel/cloudflare/__init__.py +74 -0
  16. cartography/intel/cloudflare/accounts.py +57 -0
  17. cartography/intel/cloudflare/dnsrecords.py +64 -0
  18. cartography/intel/cloudflare/members.py +75 -0
  19. cartography/intel/cloudflare/roles.py +65 -0
  20. cartography/intel/cloudflare/zones.py +64 -0
  21. cartography/intel/openai/__init__.py +86 -0
  22. cartography/intel/openai/adminapikeys.py +89 -0
  23. cartography/intel/openai/apikeys.py +96 -0
  24. cartography/intel/openai/projects.py +97 -0
  25. cartography/intel/openai/serviceaccounts.py +82 -0
  26. cartography/intel/openai/users.py +75 -0
  27. cartography/intel/openai/util.py +45 -0
  28. cartography/intel/tailscale/__init__.py +77 -0
  29. cartography/intel/tailscale/acls.py +146 -0
  30. cartography/intel/tailscale/devices.py +127 -0
  31. cartography/intel/tailscale/postureintegrations.py +81 -0
  32. cartography/intel/tailscale/tailnets.py +76 -0
  33. cartography/intel/tailscale/users.py +80 -0
  34. cartography/intel/tailscale/utils.py +132 -0
  35. cartography/models/anthropic/__init__.py +0 -0
  36. cartography/models/anthropic/apikey.py +90 -0
  37. cartography/models/anthropic/organization.py +19 -0
  38. cartography/models/anthropic/user.py +48 -0
  39. cartography/models/anthropic/workspace.py +90 -0
  40. cartography/models/aws/cloudwatch/__init__.py +0 -0
  41. cartography/models/aws/cloudwatch/loggroup.py +52 -0
  42. cartography/models/aws/efs/__init__.py +0 -0
  43. cartography/models/aws/efs/mount_target.py +52 -0
  44. cartography/models/aws/secretsmanager/__init__.py +0 -0
  45. cartography/models/aws/secretsmanager/secret_version.py +116 -0
  46. cartography/models/aws/ssm/parameters.py +84 -0
  47. cartography/models/cloudflare/__init__.py +0 -0
  48. cartography/models/cloudflare/account.py +25 -0
  49. cartography/models/cloudflare/dnsrecord.py +55 -0
  50. cartography/models/cloudflare/member.py +82 -0
  51. cartography/models/cloudflare/role.py +44 -0
  52. cartography/models/cloudflare/zone.py +59 -0
  53. cartography/models/openai/__init__.py +0 -0
  54. cartography/models/openai/adminapikey.py +90 -0
  55. cartography/models/openai/apikey.py +84 -0
  56. cartography/models/openai/organization.py +17 -0
  57. cartography/models/openai/project.py +89 -0
  58. cartography/models/openai/serviceaccount.py +50 -0
  59. cartography/models/openai/user.py +49 -0
  60. cartography/models/tailscale/__init__.py +0 -0
  61. cartography/models/tailscale/device.py +95 -0
  62. cartography/models/tailscale/group.py +86 -0
  63. cartography/models/tailscale/postureintegration.py +58 -0
  64. cartography/models/tailscale/tag.py +102 -0
  65. cartography/models/tailscale/tailnet.py +29 -0
  66. cartography/models/tailscale/user.py +52 -0
  67. cartography/sync.py +8 -0
  68. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc2.dist-info}/METADATA +8 -4
  69. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc2.dist-info}/RECORD +73 -14
  70. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc2.dist-info}/WHEEL +1 -1
  71. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc2.dist-info}/entry_points.txt +0 -0
  72. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc2.dist-info}/licenses/LICENSE +0 -0
  73. {cartography-0.103.0rc1.dist-info → cartography-0.104.0rc2.dist-info}/top_level.txt +0 -0
cartography/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.103.0rc1'
21
- __version_tuple__ = version_tuple = (0, 103, 0, 'rc1')
20
+ __version__ = version = '0.104.0rc2'
21
+ __version_tuple__ = version_tuple = (0, 104, 0, 'rc2')
cartography/cli.py CHANGED
@@ -561,7 +561,7 @@ class CLI:
561
561
  type=str,
562
562
  default=None,
563
563
  help=(
564
- "Your SnipeIT base URI"
564
+ "Your SnipeIT base URI. "
565
565
  "Required if you are using the SnipeIT intel module. Ignored otherwise."
566
566
  ),
567
567
  )
@@ -577,6 +577,66 @@ class CLI:
577
577
  default=None,
578
578
  help="An ID for the SnipeIT tenant.",
579
579
  )
580
+ parser.add_argument(
581
+ "--cloudflare-token-env-var",
582
+ type=str,
583
+ default=None,
584
+ help="The name of an environment variable containing ApiKey with which to authenticate to Cloudflare.",
585
+ )
586
+ parser.add_argument(
587
+ "--tailscale-token-env-var",
588
+ type=str,
589
+ default=None,
590
+ help=(
591
+ "The name of an environment variable containing a Tailscale API token. "
592
+ "Required if you are using the Tailscale intel module. Ignored otherwise."
593
+ ),
594
+ )
595
+ parser.add_argument(
596
+ "--tailscale-org",
597
+ type=str,
598
+ default=None,
599
+ help=(
600
+ "The name of the Tailscale organization to sync. "
601
+ "Required if you are using the Tailscale intel module. Ignored otherwise."
602
+ ),
603
+ )
604
+ parser.add_argument(
605
+ "--tailscale-base-url",
606
+ type=str,
607
+ default="https://api.tailscale.com/api/v2",
608
+ help=(
609
+ "The base URL for the Tailscale API. "
610
+ "Required if you are using the Tailscale intel module. Ignored otherwise."
611
+ ),
612
+ )
613
+ parser.add_argument(
614
+ "--openai-apikey-env-var",
615
+ type=str,
616
+ default=None,
617
+ help=(
618
+ "The name of an environment variable containing a OpenAI API Key. "
619
+ "Required if you are using the OpenAI intel module. Ignored otherwise."
620
+ ),
621
+ )
622
+ parser.add_argument(
623
+ "--openai-org-id",
624
+ type=str,
625
+ default=None,
626
+ help=(
627
+ "The ID of the OpenAI organization to sync. "
628
+ "Required if you are using the OpenAI intel module. Ignored otherwise."
629
+ ),
630
+ )
631
+ parser.add_argument(
632
+ "--anthropic-apikey-env-var",
633
+ type=str,
634
+ default=None,
635
+ help=(
636
+ "The name of an environment variable containing an Anthropic API Key. "
637
+ "Required if you are using the Anthropic intel module. Ignored otherwise."
638
+ ),
639
+ )
580
640
 
581
641
  return parser
582
642
 
@@ -859,6 +919,42 @@ class CLI:
859
919
  logger.warning("A SnipeIT base URI was not provided.")
860
920
  config.snipeit_base_uri = None
861
921
 
922
+ # Tailscale config
923
+ if config.tailscale_token_env_var:
924
+ logger.debug(
925
+ f"Reading Tailscale API token from environment variable {config.tailscale_token_env_var}",
926
+ )
927
+ config.tailscale_token = os.environ.get(config.tailscale_token_env_var)
928
+ else:
929
+ config.tailscale_token = None
930
+
931
+ # Cloudflare config
932
+ if config.cloudflare_token_env_var:
933
+ logger.debug(
934
+ f"Reading Cloudflare ApiKey from environment variable {config.cloudflare_token_env_var}",
935
+ )
936
+ config.cloudflare_token = os.environ.get(config.cloudflare_token_env_var)
937
+ else:
938
+ config.cloudflare_token = None
939
+
940
+ # OpenAI config
941
+ if config.openai_apikey_env_var:
942
+ logger.debug(
943
+ f"Reading OpenAI API key from environment variable {config.openai_apikey_env_var}",
944
+ )
945
+ config.openai_apikey = os.environ.get(config.openai_apikey_env_var)
946
+ else:
947
+ config.openai_apikey = None
948
+
949
+ # Anthropic config
950
+ if config.anthropic_apikey_env_var:
951
+ logger.debug(
952
+ f"Reading Anthropic API key from environment variable {config.anthropic_apikey_env_var}",
953
+ )
954
+ config.anthropic_apikey = os.environ.get(config.anthropic_apikey_env_var)
955
+ else:
956
+ config.anthropic_apikey = None
957
+
862
958
  # Run cartography
863
959
  try:
864
960
  return cartography.sync.run_with_config(self.sync, config)
cartography/config.py CHANGED
@@ -123,6 +123,20 @@ class Config:
123
123
  :param snipeit_token: Token used to authenticate to the SnipeIT data provider. Optional.
124
124
  :type snipeit_tenant_id: string
125
125
  :param snipeit_tenant_id: Token used to authenticate to the SnipeIT data provider. Optional.
126
+ :type tailscale_token: str
127
+ :param tailscale_token: Tailscale API token. Optional.
128
+ :type tailscale_org: str
129
+ :param tailscale_org: Tailscale organization name. Optional.
130
+ :type tailscale_base_url: str
131
+ :param tailscale_base_url: Tailscale API base URL. Optional.
132
+ :type cloudflare_token: string
133
+ :param cloudflare_token: Cloudflare API key. Optional.
134
+ :type openai_apikey: string
135
+ :param openai_apikey: OpenAI API key. Optional.
136
+ :type openai_org_id: string
137
+ :param openai_org_id: OpenAI organization id. Optional.
138
+ :type anthropic_apikey: string
139
+ :param anthropic_apikey: Anthropic API key. Optional.
126
140
  """
127
141
 
128
142
  def __init__(
@@ -188,6 +202,13 @@ class Config:
188
202
  snipeit_base_uri=None,
189
203
  snipeit_token=None,
190
204
  snipeit_tenant_id=None,
205
+ tailscale_token=None,
206
+ tailscale_org=None,
207
+ tailscale_base_url=None,
208
+ cloudflare_token=None,
209
+ openai_apikey=None,
210
+ openai_org_id=None,
211
+ anthropic_apikey=None,
191
212
  ):
192
213
  self.neo4j_uri = neo4j_uri
193
214
  self.neo4j_user = neo4j_user
@@ -250,3 +271,10 @@ class Config:
250
271
  self.snipeit_base_uri = snipeit_base_uri
251
272
  self.snipeit_token = snipeit_token
252
273
  self.snipeit_tenant_id = snipeit_tenant_id
274
+ self.tailscale_token = tailscale_token
275
+ self.tailscale_org = tailscale_org
276
+ self.tailscale_base_url = tailscale_base_url
277
+ self.cloudflare_token = cloudflare_token
278
+ self.openai_apikey = openai_apikey
279
+ self.openai_org_id = openai_org_id
280
+ self.anthropic_apikey = anthropic_apikey
@@ -0,0 +1,62 @@
1
+ import logging
2
+
3
+ import neo4j
4
+ import requests
5
+
6
+ import cartography.intel.anthropic.apikeys
7
+ import cartography.intel.anthropic.users
8
+ import cartography.intel.anthropic.workspaces
9
+ from cartography.config import Config
10
+ from cartography.util import timeit
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @timeit
16
+ def start_anthropic_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
17
+ """
18
+ If this module is configured, perform ingestion of Anthropic data. Otherwise warn and exit
19
+ :param neo4j_session: Neo4J session for database interface
20
+ :param config: A cartography.config object
21
+ :return: None
22
+ """
23
+
24
+ if not config.anthropic_apikey:
25
+ logger.info(
26
+ "Anthropic import is not configured - skipping this module. "
27
+ "See docs to configure.",
28
+ )
29
+ return
30
+
31
+ # Create requests sessions
32
+ api_session = requests.session()
33
+ api_session.headers.update(
34
+ {
35
+ "X-Api-Key": config.anthropic_apikey,
36
+ "anthropic-version": "2023-06-01",
37
+ }
38
+ )
39
+
40
+ common_job_parameters = {
41
+ "UPDATE_TAG": config.update_tag,
42
+ "BASE_URL": "https://api.anthropic.com/v1",
43
+ }
44
+
45
+ # Organization node is created during the users sync
46
+ cartography.intel.anthropic.users.sync(
47
+ neo4j_session,
48
+ api_session,
49
+ common_job_parameters,
50
+ )
51
+
52
+ cartography.intel.anthropic.workspaces.sync(
53
+ neo4j_session,
54
+ api_session,
55
+ common_job_parameters,
56
+ )
57
+
58
+ cartography.intel.anthropic.apikeys.sync(
59
+ neo4j_session,
60
+ api_session,
61
+ common_job_parameters,
62
+ )
@@ -0,0 +1,72 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Tuple
4
+
5
+ import neo4j
6
+ import requests
7
+
8
+ from cartography.client.core.tx import load
9
+ from cartography.graph.job import GraphJob
10
+ from cartography.intel.anthropic.util import paginated_get
11
+ from cartography.models.anthropic.apikey import AnthropicApiKeySchema
12
+ from cartography.util import timeit
13
+
14
+ logger = logging.getLogger(__name__)
15
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
16
+ _TIMEOUT = (60, 60)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ api_session: requests.Session,
23
+ common_job_parameters: dict[str, Any],
24
+ ) -> None:
25
+ org_id, apikeys = get(
26
+ api_session,
27
+ common_job_parameters["BASE_URL"],
28
+ )
29
+ common_job_parameters["ORG_ID"] = org_id
30
+ load_apikeys(
31
+ neo4j_session,
32
+ apikeys,
33
+ org_id,
34
+ common_job_parameters["UPDATE_TAG"],
35
+ )
36
+ cleanup(neo4j_session, common_job_parameters)
37
+
38
+
39
+ @timeit
40
+ def get(
41
+ api_session: requests.Session,
42
+ base_url: str,
43
+ ) -> Tuple[str, list[dict[str, Any]]]:
44
+ return paginated_get(
45
+ api_session, f"{base_url}/organizations/api_keys", timeout=_TIMEOUT
46
+ )
47
+
48
+
49
+ @timeit
50
+ def load_apikeys(
51
+ neo4j_session: neo4j.Session,
52
+ data: list[dict[str, Any]],
53
+ ORG_ID: str,
54
+ update_tag: int,
55
+ ) -> None:
56
+ logger.info("Loading %d Anthropic ApiKey into Neo4j.", len(data))
57
+ load(
58
+ neo4j_session,
59
+ AnthropicApiKeySchema(),
60
+ data,
61
+ lastupdated=update_tag,
62
+ ORG_ID=ORG_ID,
63
+ )
64
+
65
+
66
+ @timeit
67
+ def cleanup(
68
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
69
+ ) -> None:
70
+ GraphJob.from_node_schema(AnthropicApiKeySchema(), common_job_parameters).run(
71
+ neo4j_session
72
+ )
@@ -0,0 +1,75 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Tuple
4
+
5
+ import neo4j
6
+ import requests
7
+
8
+ from cartography.client.core.tx import load
9
+ from cartography.graph.job import GraphJob
10
+ from cartography.intel.anthropic.util import paginated_get
11
+ from cartography.models.anthropic.organization import AnthropicOrganizationSchema
12
+ from cartography.models.anthropic.user import AnthropicUserSchema
13
+ from cartography.util import timeit
14
+
15
+ logger = logging.getLogger(__name__)
16
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
17
+ _TIMEOUT = (60, 60)
18
+
19
+
20
+ @timeit
21
+ def sync(
22
+ neo4j_session: neo4j.Session,
23
+ api_session: requests.Session,
24
+ common_job_parameters: dict[str, Any],
25
+ ) -> str:
26
+ org_id, users = get(
27
+ api_session,
28
+ common_job_parameters["BASE_URL"],
29
+ )
30
+ common_job_parameters["ORG_ID"] = org_id
31
+ load_users(neo4j_session, users, org_id, common_job_parameters["UPDATE_TAG"])
32
+ cleanup(neo4j_session, common_job_parameters)
33
+ return org_id
34
+
35
+
36
+ @timeit
37
+ def get(
38
+ api_session: requests.Session,
39
+ base_url: str,
40
+ ) -> Tuple[str, list[dict[str, Any]]]:
41
+ return paginated_get(
42
+ api_session, f"{base_url}/organizations/users", timeout=_TIMEOUT
43
+ )
44
+
45
+
46
+ @timeit
47
+ def load_users(
48
+ neo4j_session: neo4j.Session,
49
+ data: list[dict[str, Any]],
50
+ ORG_ID: str,
51
+ update_tag: int,
52
+ ) -> None:
53
+ load(
54
+ neo4j_session,
55
+ AnthropicOrganizationSchema(),
56
+ [{"id": ORG_ID}],
57
+ lastupdated=update_tag,
58
+ )
59
+ logger.info("Loading %d Anthropic User into Neo4j.", len(data))
60
+ load(
61
+ neo4j_session,
62
+ AnthropicUserSchema(),
63
+ data,
64
+ lastupdated=update_tag,
65
+ ORG_ID=ORG_ID,
66
+ )
67
+
68
+
69
+ @timeit
70
+ def cleanup(
71
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
72
+ ) -> None:
73
+ GraphJob.from_node_schema(AnthropicUserSchema(), common_job_parameters).run(
74
+ neo4j_session
75
+ )
@@ -0,0 +1,51 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+
6
+ def paginated_get(
7
+ api_session: requests.Session,
8
+ url: str,
9
+ timeout: tuple[int, int],
10
+ after: str | None = None,
11
+ ) -> tuple[str, list[dict[str, Any]]]:
12
+ """Helper function to get paginated data from the Anthropic API.
13
+
14
+ This function handles the pagination of the API requests and returns
15
+ the results in a list. It also retrieves the organization ID from the
16
+ response headers. The function will continue to make requests until
17
+ all pages of data have been retrieved. The results are returned as a
18
+ list of dictionaries, where each dictionary represents a single
19
+ entity.
20
+
21
+ Args:
22
+ api_session (requests.Session): The requests session to use for making API calls.
23
+ url (str): The URL to make the API call to.
24
+ timeout (tuple[int, int]): The timeout for the API call.
25
+ after (str | None): The ID of the last item retrieved in the previous request.
26
+ If None, the first page of results will be retrieved.
27
+ Returns:
28
+ tuple[str, list[dict[str, Any]]]: A tuple containing the organization ID and a list of
29
+ dictionaries representing the results.
30
+ """
31
+ results: list[dict[str, Any]] = []
32
+ params = {"after_id": after} if after else {}
33
+ req = api_session.get(
34
+ url,
35
+ params=params,
36
+ timeout=timeout,
37
+ )
38
+ req.raise_for_status()
39
+ # Get organization_id from the headers
40
+ organization_id = req.headers.get("anthropic-organization-id", "")
41
+ result = req.json()
42
+ results.extend(result.get("data", []))
43
+ if result.get("has_more"):
44
+ _, next_results = paginated_get(
45
+ api_session,
46
+ url,
47
+ timeout=timeout,
48
+ after=result.get("last_id"),
49
+ )
50
+ results.extend(next_results)
51
+ return organization_id, results
@@ -0,0 +1,95 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Tuple
4
+
5
+ import neo4j
6
+ import requests
7
+
8
+ from cartography.client.core.tx import load
9
+ from cartography.graph.job import GraphJob
10
+ from cartography.intel.anthropic.util import paginated_get
11
+ from cartography.models.anthropic.workspace import AnthropicWorkspaceSchema
12
+ from cartography.util import timeit
13
+
14
+ logger = logging.getLogger(__name__)
15
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
16
+ _TIMEOUT = (60, 60)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ api_session: requests.Session,
23
+ common_job_parameters: dict[str, Any],
24
+ ) -> list[dict]:
25
+ org_id, workspaces = get(
26
+ api_session,
27
+ common_job_parameters["BASE_URL"],
28
+ )
29
+ common_job_parameters["ORG_ID"] = org_id
30
+ for workspace in workspaces:
31
+ workspace["users"] = []
32
+ workspace["admins"] = []
33
+ for user in get_workspace_users(
34
+ api_session,
35
+ common_job_parameters["BASE_URL"],
36
+ workspace["id"],
37
+ ):
38
+ workspace["users"].append(user["user_id"])
39
+ if user["workspace_role"] == "workspace_admin":
40
+ workspace["admins"].append(user["user_id"])
41
+ load_workspaces(
42
+ neo4j_session, workspaces, org_id, common_job_parameters["UPDATE_TAG"]
43
+ )
44
+ cleanup(neo4j_session, common_job_parameters)
45
+ return workspaces
46
+
47
+
48
+ @timeit
49
+ def get(
50
+ api_session: requests.Session,
51
+ base_url: str,
52
+ ) -> Tuple[str, list[dict[str, Any]]]:
53
+ return paginated_get(
54
+ api_session, f"{base_url}/organizations/workspaces", timeout=_TIMEOUT
55
+ )
56
+
57
+
58
+ @timeit
59
+ def get_workspace_users(
60
+ api_session: requests.Session,
61
+ base_url: str,
62
+ workspace_id: str,
63
+ ) -> list[dict[str, Any]]:
64
+ _, result = paginated_get(
65
+ api_session,
66
+ f"{base_url}/organizations/workspaces/{workspace_id}/members",
67
+ timeout=_TIMEOUT,
68
+ )
69
+ return result
70
+
71
+
72
+ @timeit
73
+ def load_workspaces(
74
+ neo4j_session: neo4j.Session,
75
+ data: list[dict[str, Any]],
76
+ ORG_ID: str,
77
+ update_tag: int,
78
+ ) -> None:
79
+ logger.info("Loading %d Anthropic workspaces into Neo4j.", len(data))
80
+ load(
81
+ neo4j_session,
82
+ AnthropicWorkspaceSchema(),
83
+ data,
84
+ lastupdated=update_tag,
85
+ ORG_ID=ORG_ID,
86
+ )
87
+
88
+
89
+ @timeit
90
+ def cleanup(
91
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
92
+ ) -> None:
93
+ GraphJob.from_node_schema(AnthropicWorkspaceSchema(), common_job_parameters).run(
94
+ neo4j_session
95
+ )
@@ -0,0 +1,93 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import boto3
7
+ import neo4j
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.aws.ec2.util import get_botocore_config
12
+ from cartography.models.aws.cloudwatch.loggroup import CloudWatchLogGroupSchema
13
+ from cartography.util import aws_handle_regions
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ @aws_handle_regions
21
+ def get_cloudwatch_log_groups(
22
+ boto3_session: boto3.Session, region: str
23
+ ) -> List[Dict[str, Any]]:
24
+ client = boto3_session.client(
25
+ "logs", region_name=region, config=get_botocore_config()
26
+ )
27
+ paginator = client.get_paginator("describe_log_groups")
28
+ logGroups = []
29
+ for page in paginator.paginate():
30
+ logGroups.extend(page["logGroups"])
31
+ return logGroups
32
+
33
+
34
+ @timeit
35
+ def load_cloudwatch_log_groups(
36
+ neo4j_session: neo4j.Session,
37
+ data: List[Dict[str, Any]],
38
+ region: str,
39
+ current_aws_account_id: str,
40
+ aws_update_tag: int,
41
+ ) -> None:
42
+ logger.info(
43
+ f"Loading CloudWatch {len(data)} log groups for region '{region}' into graph.",
44
+ )
45
+ load(
46
+ neo4j_session,
47
+ CloudWatchLogGroupSchema(),
48
+ data,
49
+ lastupdated=aws_update_tag,
50
+ Region=region,
51
+ AWS_ID=current_aws_account_id,
52
+ )
53
+
54
+
55
+ @timeit
56
+ def cleanup(
57
+ neo4j_session: neo4j.Session,
58
+ common_job_parameters: Dict[str, Any],
59
+ ) -> None:
60
+ logger.debug("Running CloudWatch cleanup job.")
61
+ cleanup_job = GraphJob.from_node_schema(
62
+ CloudWatchLogGroupSchema(), common_job_parameters
63
+ )
64
+ cleanup_job.run(neo4j_session)
65
+
66
+
67
+ @timeit
68
+ def sync(
69
+ neo4j_session: neo4j.Session,
70
+ boto3_session: boto3.session.Session,
71
+ regions: List[str],
72
+ current_aws_account_id: str,
73
+ update_tag: int,
74
+ common_job_parameters: Dict[str, Any],
75
+ ) -> None:
76
+ for region in regions:
77
+ logger.info(
78
+ f"Syncing CloudWatch for region '{region}' in account '{current_aws_account_id}'.",
79
+ )
80
+ logGroups = get_cloudwatch_log_groups(boto3_session, region)
81
+ group_data: List[Dict[str, Any]] = []
82
+ for logGroup in logGroups:
83
+ group_data.append(logGroup)
84
+
85
+ load_cloudwatch_log_groups(
86
+ neo4j_session,
87
+ group_data,
88
+ region,
89
+ current_aws_account_id,
90
+ update_tag,
91
+ )
92
+
93
+ cleanup(neo4j_session, common_job_parameters)
@@ -99,7 +99,10 @@ def load_load_balancer_v2s(
99
99
  SET r.lastupdated = $update_tag
100
100
  """
101
101
  for lb in data:
102
- load_balancer_id = lb["DNSName"]
102
+ load_balancer_id = lb.get("DNSName")
103
+ if not load_balancer_id:
104
+ logger.warning("Skipping load balancer entry with missing DNSName: %r", lb)
105
+ continue
103
106
 
104
107
  neo4j_session.run(
105
108
  ingest_load_balancer_v2,