cartography 0.114.0__py3-none-any.whl → 0.116.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.
Potentially problematic release.
This version of cartography might be problematic. Click here for more details.
- cartography/_version.py +2 -2
- cartography/cli.py +2 -2
- cartography/client/core/tx.py +12 -1
- cartography/intel/aws/config.py +7 -3
- cartography/intel/aws/ecr.py +9 -9
- cartography/intel/aws/ecr_image_layers.py +664 -0
- cartography/intel/aws/identitycenter.py +240 -13
- cartography/intel/aws/lambda_function.py +69 -2
- cartography/intel/aws/organizations.py +3 -1
- cartography/intel/aws/permission_relationships.py +3 -1
- cartography/intel/aws/redshift.py +9 -4
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/route53.py +53 -3
- cartography/intel/aws/securityhub.py +3 -1
- cartography/intel/azure/__init__.py +16 -0
- cartography/intel/azure/logic_apps.py +101 -0
- cartography/intel/azure/resource_groups.py +82 -0
- cartography/intel/create_indexes.py +2 -1
- cartography/intel/dns.py +5 -2
- cartography/intel/gcp/dns.py +2 -1
- cartography/intel/github/repos.py +3 -6
- cartography/intel/gsuite/api.py +17 -4
- cartography/intel/okta/applications.py +9 -4
- cartography/intel/okta/awssaml.py +5 -2
- cartography/intel/okta/factors.py +3 -1
- cartography/intel/okta/groups.py +5 -2
- cartography/intel/okta/organization.py +3 -1
- cartography/intel/okta/origins.py +3 -1
- cartography/intel/okta/roles.py +5 -2
- cartography/intel/okta/users.py +3 -1
- cartography/models/aws/ecr/image.py +21 -0
- cartography/models/aws/ecr/image_layer.py +107 -0
- cartography/models/aws/identitycenter/awspermissionset.py +24 -1
- cartography/models/aws/identitycenter/awssogroup.py +70 -0
- cartography/models/aws/identitycenter/awsssouser.py +37 -1
- cartography/models/aws/lambda_function/lambda_function.py +2 -0
- cartography/models/azure/logic_apps.py +56 -0
- cartography/models/azure/resource_groups.py +52 -0
- cartography/models/entra/user.py +18 -0
- cartography/rules/README.md +1 -0
- cartography/rules/__init__.py +0 -0
- cartography/rules/cli.py +342 -0
- cartography/rules/data/__init__.py +0 -0
- cartography/rules/data/frameworks/__init__.py +12 -0
- cartography/rules/data/frameworks/mitre_attack/__init__.py +14 -0
- cartography/rules/data/frameworks/mitre_attack/requirements/__init__.py +0 -0
- cartography/rules/data/frameworks/mitre_attack/requirements/t1190_exploit_public_facing_application/__init__.py +135 -0
- cartography/rules/formatters.py +46 -0
- cartography/rules/runners.py +338 -0
- cartography/rules/spec/__init__.py +0 -0
- cartography/rules/spec/model.py +88 -0
- cartography/rules/spec/result.py +46 -0
- {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/METADATA +19 -4
- {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/RECORD +58 -38
- {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/entry_points.txt +1 -0
- {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/WHEEL +0 -0
- {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework and Fact execution logic for Cartography rules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
|
|
8
|
+
from neo4j import Driver
|
|
9
|
+
from neo4j import GraphDatabase
|
|
10
|
+
|
|
11
|
+
from cartography.client.core.tx import read_list_of_dicts_tx
|
|
12
|
+
from cartography.rules.data.frameworks import FRAMEWORKS
|
|
13
|
+
from cartography.rules.formatters import _generate_neo4j_browser_url
|
|
14
|
+
from cartography.rules.spec.model import Fact
|
|
15
|
+
from cartography.rules.spec.model import Framework
|
|
16
|
+
from cartography.rules.spec.model import Requirement
|
|
17
|
+
from cartography.rules.spec.result import FactResult
|
|
18
|
+
from cartography.rules.spec.result import FrameworkResult
|
|
19
|
+
from cartography.rules.spec.result import RequirementResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _run_fact(
|
|
23
|
+
fact: Fact,
|
|
24
|
+
requirement: Requirement,
|
|
25
|
+
framework: Framework,
|
|
26
|
+
driver: Driver,
|
|
27
|
+
database: str,
|
|
28
|
+
fact_counter: int,
|
|
29
|
+
total_facts: int,
|
|
30
|
+
output_format: str,
|
|
31
|
+
neo4j_uri: str,
|
|
32
|
+
):
|
|
33
|
+
"""Execute a single fact and return the result."""
|
|
34
|
+
if output_format == "text":
|
|
35
|
+
print(f"\n\033[1mFact {fact_counter}/{total_facts}: {fact.name}\033[0m")
|
|
36
|
+
print(f" \033[36m{'Framework:':<12}\033[0m {framework.name}")
|
|
37
|
+
# Display requirement with optional clickable link
|
|
38
|
+
if requirement.requirement_url:
|
|
39
|
+
print(
|
|
40
|
+
f" \033[36m{'Requirement:':<12}\033[0m \033]8;;{requirement.requirement_url}\033\\{requirement.id}\033]8;;\033\\ - {requirement.name}"
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
print(
|
|
44
|
+
f" \033[36m{'Requirement:':<12}\033[0m {requirement.id} - {requirement.name}"
|
|
45
|
+
)
|
|
46
|
+
print(f" \033[36m{'Fact ID:':<12}\033[0m {fact.id}")
|
|
47
|
+
print(f" \033[36m{'Description:':<12}\033[0m {fact.description}")
|
|
48
|
+
print(f" \033[36m{'Provider:':<12}\033[0m {fact.module.value}")
|
|
49
|
+
|
|
50
|
+
# Generate and display clickable Neo4j Browser URL
|
|
51
|
+
browser_url = _generate_neo4j_browser_url(neo4j_uri, fact.cypher_visual_query)
|
|
52
|
+
print(
|
|
53
|
+
f" \033[36m{'Neo4j Query:':<12}\033[0m \033]8;;{browser_url}\033\\Click to run visual query\033]8;;\033\\"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
with driver.session(database=database) as session:
|
|
57
|
+
findings = session.execute_read(read_list_of_dicts_tx, fact.cypher_query)
|
|
58
|
+
finding_count = len(findings)
|
|
59
|
+
|
|
60
|
+
if output_format == "text":
|
|
61
|
+
if finding_count > 0:
|
|
62
|
+
print(f" \033[36m{'Results:':<12}\033[0m {finding_count} item(s) found")
|
|
63
|
+
|
|
64
|
+
# Show sample findings
|
|
65
|
+
print(" Sample results:")
|
|
66
|
+
for idx, finding in enumerate(findings[:3]): # Show first 3
|
|
67
|
+
# Format finding nicely
|
|
68
|
+
formatted_items = []
|
|
69
|
+
for key, value in finding.items():
|
|
70
|
+
if value is not None:
|
|
71
|
+
# Truncate long values
|
|
72
|
+
str_value = str(value)
|
|
73
|
+
if len(str_value) > 50:
|
|
74
|
+
str_value = str_value[:47] + "..."
|
|
75
|
+
formatted_items.append(f"{key}={str_value}")
|
|
76
|
+
|
|
77
|
+
if formatted_items:
|
|
78
|
+
print(f" {idx + 1}. {', '.join(formatted_items)}")
|
|
79
|
+
|
|
80
|
+
if finding_count > 3:
|
|
81
|
+
print(
|
|
82
|
+
f" ... and {finding_count - 3} more (use --output json to see all)"
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
print(f" \033[36m{'Results:':<12}\033[0m No items found")
|
|
86
|
+
|
|
87
|
+
# Create and return fact result
|
|
88
|
+
return FactResult(
|
|
89
|
+
fact_id=fact.id,
|
|
90
|
+
fact_name=fact.name,
|
|
91
|
+
fact_description=fact.description,
|
|
92
|
+
fact_provider=fact.module.value,
|
|
93
|
+
finding_count=finding_count,
|
|
94
|
+
findings=findings if output_format == "json" else findings[:10],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run_single_requirement(
|
|
99
|
+
requirement: Requirement,
|
|
100
|
+
framework: Framework,
|
|
101
|
+
driver: Driver,
|
|
102
|
+
database: str,
|
|
103
|
+
output_format: str,
|
|
104
|
+
neo4j_uri: str,
|
|
105
|
+
fact_counter_start: int,
|
|
106
|
+
total_facts: int,
|
|
107
|
+
fact_filter: str | None = None,
|
|
108
|
+
) -> tuple[RequirementResult, int]:
|
|
109
|
+
"""
|
|
110
|
+
Execute a single requirement and return its result.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A tuple of (RequirementResult, facts_executed_count)
|
|
114
|
+
"""
|
|
115
|
+
# Filter facts if needed
|
|
116
|
+
facts_to_run = requirement.facts
|
|
117
|
+
if fact_filter:
|
|
118
|
+
facts_to_run = tuple(
|
|
119
|
+
f for f in requirement.facts if f.id.lower() == fact_filter.lower()
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
fact_results = []
|
|
123
|
+
requirement_findings = 0
|
|
124
|
+
fact_counter = fact_counter_start
|
|
125
|
+
|
|
126
|
+
for fact in facts_to_run:
|
|
127
|
+
fact_counter += 1
|
|
128
|
+
fact_result = _run_fact(
|
|
129
|
+
fact,
|
|
130
|
+
requirement,
|
|
131
|
+
framework,
|
|
132
|
+
driver,
|
|
133
|
+
database,
|
|
134
|
+
fact_counter,
|
|
135
|
+
total_facts,
|
|
136
|
+
output_format,
|
|
137
|
+
neo4j_uri,
|
|
138
|
+
)
|
|
139
|
+
fact_results.append(fact_result)
|
|
140
|
+
requirement_findings += fact_result.finding_count
|
|
141
|
+
|
|
142
|
+
# Create requirement result
|
|
143
|
+
requirement_result = RequirementResult(
|
|
144
|
+
requirement_id=requirement.id,
|
|
145
|
+
requirement_name=requirement.name,
|
|
146
|
+
requirement_url=requirement.requirement_url,
|
|
147
|
+
facts=fact_results,
|
|
148
|
+
total_facts=len(fact_results),
|
|
149
|
+
total_findings=requirement_findings,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return requirement_result, len(facts_to_run)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _run_single_framework(
|
|
156
|
+
framework_name: str,
|
|
157
|
+
driver: GraphDatabase.driver,
|
|
158
|
+
database: str,
|
|
159
|
+
output_format: str,
|
|
160
|
+
neo4j_uri: str,
|
|
161
|
+
requirement_filter: str | None = None,
|
|
162
|
+
fact_filter: str | None = None,
|
|
163
|
+
) -> FrameworkResult:
|
|
164
|
+
"""Execute a single framework and return results."""
|
|
165
|
+
framework = FRAMEWORKS[framework_name]
|
|
166
|
+
|
|
167
|
+
# Filter requirements if needed
|
|
168
|
+
requirements_to_run = framework.requirements
|
|
169
|
+
if requirement_filter:
|
|
170
|
+
requirements_to_run = tuple(
|
|
171
|
+
req
|
|
172
|
+
for req in framework.requirements
|
|
173
|
+
if req.id.lower() == requirement_filter.lower()
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Count total facts for display (before filtering)
|
|
177
|
+
total_facts_display = sum(len(req.facts) for req in requirements_to_run)
|
|
178
|
+
|
|
179
|
+
if output_format == "text":
|
|
180
|
+
print(f"Executing {framework.name} framework")
|
|
181
|
+
if requirement_filter:
|
|
182
|
+
print(f"Filtered to requirement: {requirement_filter}")
|
|
183
|
+
if fact_filter:
|
|
184
|
+
print(f"Filtered to fact: {fact_filter}")
|
|
185
|
+
print(f"Requirements: {len(requirements_to_run)}")
|
|
186
|
+
print(f"Total facts: {total_facts_display}")
|
|
187
|
+
|
|
188
|
+
# Execute requirements and collect results
|
|
189
|
+
total_findings = 0
|
|
190
|
+
total_facts_executed = 0
|
|
191
|
+
requirement_results = []
|
|
192
|
+
fact_counter = 0
|
|
193
|
+
|
|
194
|
+
for requirement in requirements_to_run:
|
|
195
|
+
requirement_result, facts_executed = _run_single_requirement(
|
|
196
|
+
requirement,
|
|
197
|
+
framework,
|
|
198
|
+
driver,
|
|
199
|
+
database,
|
|
200
|
+
output_format,
|
|
201
|
+
neo4j_uri,
|
|
202
|
+
fact_counter,
|
|
203
|
+
total_facts_display,
|
|
204
|
+
fact_filter,
|
|
205
|
+
)
|
|
206
|
+
requirement_results.append(requirement_result)
|
|
207
|
+
total_findings += requirement_result.total_findings
|
|
208
|
+
total_facts_executed += facts_executed
|
|
209
|
+
fact_counter += facts_executed
|
|
210
|
+
|
|
211
|
+
# Create and return framework result
|
|
212
|
+
return FrameworkResult(
|
|
213
|
+
framework_id=framework.id,
|
|
214
|
+
framework_name=framework.name,
|
|
215
|
+
framework_version=framework.version,
|
|
216
|
+
requirements=requirement_results,
|
|
217
|
+
total_requirements=len(requirements_to_run),
|
|
218
|
+
total_facts=total_facts_executed, # Use actual executed count
|
|
219
|
+
total_findings=total_findings,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _format_and_output_results(
|
|
224
|
+
all_results: list[FrameworkResult],
|
|
225
|
+
framework_names: list[str],
|
|
226
|
+
output_format: str,
|
|
227
|
+
total_requirements: int,
|
|
228
|
+
total_facts: int,
|
|
229
|
+
total_findings: int,
|
|
230
|
+
):
|
|
231
|
+
"""Format and output the results of framework execution."""
|
|
232
|
+
if output_format == "json":
|
|
233
|
+
combined_output = [asdict(result) for result in all_results]
|
|
234
|
+
print(json.dumps(combined_output, indent=2))
|
|
235
|
+
else:
|
|
236
|
+
# Text summary
|
|
237
|
+
print("\n" + "=" * 60)
|
|
238
|
+
if len(framework_names) == 1:
|
|
239
|
+
print(f"EXECUTION SUMMARY - {FRAMEWORKS[framework_names[0]].name}")
|
|
240
|
+
else:
|
|
241
|
+
print("OVERALL SUMMARY")
|
|
242
|
+
print("=" * 60)
|
|
243
|
+
|
|
244
|
+
if len(framework_names) > 1:
|
|
245
|
+
print(f"Frameworks executed: {len(framework_names)}")
|
|
246
|
+
print(f"Requirements: {total_requirements}")
|
|
247
|
+
print(f"Total facts: {total_facts}")
|
|
248
|
+
print(f"Total results: {total_findings}")
|
|
249
|
+
|
|
250
|
+
if total_findings > 0:
|
|
251
|
+
print(
|
|
252
|
+
f"\n\033[36mFramework execution completed with {total_findings} total results\033[0m"
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
print("\n\033[90mFramework execution completed with no results\033[0m")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_frameworks(
|
|
259
|
+
framework_names: list[str],
|
|
260
|
+
uri: str,
|
|
261
|
+
neo4j_user: str,
|
|
262
|
+
neo4j_password: str,
|
|
263
|
+
neo4j_database: str,
|
|
264
|
+
output_format: str = "text",
|
|
265
|
+
requirement_filter: str | None = None,
|
|
266
|
+
fact_filter: str | None = None,
|
|
267
|
+
):
|
|
268
|
+
"""
|
|
269
|
+
Execute the specified frameworks and present results.
|
|
270
|
+
|
|
271
|
+
:param framework_names: The names of the frameworks to execute.
|
|
272
|
+
:param uri: The URI of the Neo4j database. E.g. "bolt://localhost:7687" or "neo4j+s://tenant123.databases.neo4j.io:7687"
|
|
273
|
+
:param neo4j_user: The username for the Neo4j database.
|
|
274
|
+
:param neo4j_password: The password for the Neo4j database.
|
|
275
|
+
:param neo4j_database: The name of the Neo4j database.
|
|
276
|
+
:param output_format: Either "text" or "json". Defaults to "text".
|
|
277
|
+
:param requirement_filter: Optional requirement ID to filter execution (case-insensitive).
|
|
278
|
+
:param fact_filter: Optional fact ID to filter execution (case-insensitive).
|
|
279
|
+
:return: The exit code.
|
|
280
|
+
"""
|
|
281
|
+
# Validate all frameworks exist
|
|
282
|
+
for framework_name in framework_names:
|
|
283
|
+
if framework_name not in FRAMEWORKS:
|
|
284
|
+
if output_format == "text":
|
|
285
|
+
print(f"Unknown framework: {framework_name}")
|
|
286
|
+
print(f"Available frameworks: {', '.join(FRAMEWORKS.keys())}")
|
|
287
|
+
return 1
|
|
288
|
+
|
|
289
|
+
# Connect to Neo4j
|
|
290
|
+
if output_format == "text":
|
|
291
|
+
print(f"Connecting to Neo4j at {uri}...")
|
|
292
|
+
driver = GraphDatabase.driver(uri, auth=(neo4j_user, neo4j_password))
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
driver.verify_connectivity()
|
|
296
|
+
|
|
297
|
+
# Execute frameworks
|
|
298
|
+
all_results = []
|
|
299
|
+
total_requirements = 0
|
|
300
|
+
total_facts = 0
|
|
301
|
+
total_findings = 0
|
|
302
|
+
|
|
303
|
+
for i, framework_name in enumerate(framework_names):
|
|
304
|
+
if output_format == "text" and len(framework_names) > 1:
|
|
305
|
+
if i > 0:
|
|
306
|
+
print("\n" + "=" * 60)
|
|
307
|
+
print(
|
|
308
|
+
f"Executing framework {i + 1}/{len(framework_names)}: {framework_name}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
framework_result = _run_single_framework(
|
|
312
|
+
framework_name,
|
|
313
|
+
driver,
|
|
314
|
+
neo4j_database,
|
|
315
|
+
output_format,
|
|
316
|
+
uri,
|
|
317
|
+
requirement_filter,
|
|
318
|
+
fact_filter,
|
|
319
|
+
)
|
|
320
|
+
all_results.append(framework_result)
|
|
321
|
+
|
|
322
|
+
total_requirements += framework_result.total_requirements
|
|
323
|
+
total_facts += framework_result.total_facts
|
|
324
|
+
total_findings += framework_result.total_findings
|
|
325
|
+
|
|
326
|
+
# Output results
|
|
327
|
+
_format_and_output_results(
|
|
328
|
+
all_results,
|
|
329
|
+
framework_names,
|
|
330
|
+
output_format,
|
|
331
|
+
total_requirements,
|
|
332
|
+
total_facts,
|
|
333
|
+
total_findings,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return 0
|
|
337
|
+
finally:
|
|
338
|
+
driver.close()
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Module(str, Enum):
|
|
7
|
+
"""Services that can be monitored"""
|
|
8
|
+
|
|
9
|
+
AWS = "AWS"
|
|
10
|
+
"""Amazon Web Services"""
|
|
11
|
+
|
|
12
|
+
AZURE = "Azure"
|
|
13
|
+
"""Microsoft Azure"""
|
|
14
|
+
|
|
15
|
+
GCP = "GCP"
|
|
16
|
+
"""Google Cloud Platform"""
|
|
17
|
+
|
|
18
|
+
GITHUB = "GitHub"
|
|
19
|
+
"""GitHub source code management"""
|
|
20
|
+
|
|
21
|
+
OKTA = "Okta"
|
|
22
|
+
"""Okta identity and access management"""
|
|
23
|
+
|
|
24
|
+
CROSS_CLOUD = "CROSS_CLOUD"
|
|
25
|
+
"""Multi-cloud or provider-agnostic rules"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Fact:
|
|
30
|
+
"""A Fact gathers information about the environment using a Cypher query."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
"""A descriptive identifier for the Fact. Should be globally unique within Cartography."""
|
|
34
|
+
name: str
|
|
35
|
+
"""A descriptive name for the Fact."""
|
|
36
|
+
description: str
|
|
37
|
+
"""More details about the Fact. Information on details that we're querying for."""
|
|
38
|
+
module: Module
|
|
39
|
+
"""The Module that the Fact is associated with e.g. AWS, Azure, GCP, etc."""
|
|
40
|
+
# TODO can we lint the queries. full-on integ tests here are overkill though.
|
|
41
|
+
cypher_query: str
|
|
42
|
+
"""The Cypher query to gather information about the environment. Returns data field by field e.g. `RETURN node.prop1, node.prop2`."""
|
|
43
|
+
cypher_visual_query: str
|
|
44
|
+
"""
|
|
45
|
+
Same as `cypher_query`, returns it in a visual format for the web interface with `.. RETURN *`.
|
|
46
|
+
Often includes additional relationships to help give context.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Requirement:
|
|
52
|
+
"""
|
|
53
|
+
A requirement within a security framework with one or more facts.
|
|
54
|
+
|
|
55
|
+
Notes:
|
|
56
|
+
- `attributes` is reserved for metadata such as tags, categories, or references.
|
|
57
|
+
- Do NOT put evaluation logic, thresholds, or org-specific preferences here.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
id: str
|
|
61
|
+
name: str
|
|
62
|
+
description: str
|
|
63
|
+
facts: tuple[Fact, ...]
|
|
64
|
+
attributes: dict[str, Any] | None = None
|
|
65
|
+
"""
|
|
66
|
+
Metadata attributes for the requirement. Example:
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"tactic": "initial_access",
|
|
70
|
+
"technique_id": "T1190",
|
|
71
|
+
"services": ["ec2", "s3", "rds", "azure_storage"],
|
|
72
|
+
"providers": ["AWS", "AZURE"],
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
requirement_url: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class Framework:
|
|
81
|
+
"""A security framework containing requirements for comprehensive assessment."""
|
|
82
|
+
|
|
83
|
+
id: str
|
|
84
|
+
name: str
|
|
85
|
+
description: str
|
|
86
|
+
version: str
|
|
87
|
+
requirements: tuple[Requirement, ...]
|
|
88
|
+
source_url: str | None = None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Execution result classes
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class FactResult:
|
|
8
|
+
"""
|
|
9
|
+
Results for a single Fact.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
fact_id: str
|
|
13
|
+
fact_name: str
|
|
14
|
+
fact_description: str
|
|
15
|
+
fact_provider: str
|
|
16
|
+
finding_count: int = 0
|
|
17
|
+
findings: list[dict[str, Any]] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class RequirementResult:
|
|
22
|
+
"""
|
|
23
|
+
Results for a single requirement, containing all its Facts.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
requirement_id: str
|
|
27
|
+
requirement_name: str
|
|
28
|
+
requirement_url: str | None
|
|
29
|
+
facts: list[FactResult]
|
|
30
|
+
total_facts: int
|
|
31
|
+
total_findings: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class FrameworkResult:
|
|
36
|
+
"""
|
|
37
|
+
The formal object output by `--output json` from the `cartography-rules` CLI.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
framework_id: str
|
|
41
|
+
framework_name: str
|
|
42
|
+
framework_version: str
|
|
43
|
+
requirements: list[RequirementResult]
|
|
44
|
+
total_requirements: int
|
|
45
|
+
total_facts: int
|
|
46
|
+
total_findings: int
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cartography
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.116.0
|
|
4
4
|
Summary: Explore assets and their relationships across your technical infrastructure.
|
|
5
5
|
Maintainer: Cartography Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,9 +23,11 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: backoff>=2.1.2
|
|
25
25
|
Requires-Dist: boto3>=1.15.1
|
|
26
|
+
Requires-Dist: aioboto3>=13.0.0
|
|
27
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
28
|
Requires-Dist: botocore>=1.18.1
|
|
27
29
|
Requires-Dist: dnspython>=1.15.0
|
|
28
|
-
Requires-Dist: neo4j
|
|
30
|
+
Requires-Dist: neo4j<6.0.0,>=5.28.2
|
|
29
31
|
Requires-Dist: policyuniverse>=1.1.0.0
|
|
30
32
|
Requires-Dist: google-api-python-client>=1.7.8
|
|
31
33
|
Requires-Dist: google-auth>=2.37.0
|
|
@@ -43,6 +45,7 @@ Requires-Dist: azure-mgmt-compute>=5.0.0
|
|
|
43
45
|
Requires-Dist: azure-mgmt-resource>=10.2.0
|
|
44
46
|
Requires-Dist: azure-mgmt-cosmosdb>=6.0.0
|
|
45
47
|
Requires-Dist: azure-mgmt-web>=7.0.0
|
|
48
|
+
Requires-Dist: azure-mgmt-logic>=10.0.0
|
|
46
49
|
Requires-Dist: msrestazure>=0.6.4
|
|
47
50
|
Requires-Dist: azure-mgmt-storage>=16.0.0
|
|
48
51
|
Requires-Dist: azure-mgmt-sql<=1.0.0
|
|
@@ -57,6 +60,7 @@ Requires-Dist: duo-client
|
|
|
57
60
|
Requires-Dist: cloudflare<5.0.0,>=4.1.0
|
|
58
61
|
Requires-Dist: scaleway>=2.9.0
|
|
59
62
|
Requires-Dist: google-cloud-resource-manager>=1.14.2
|
|
63
|
+
Requires-Dist: typer>=0.9.0
|
|
60
64
|
Dynamic: license-file
|
|
61
65
|
|
|
62
66
|

|
|
@@ -83,7 +87,7 @@ You can learn more about the story behind Cartography in our [presentation at BS
|
|
|
83
87
|
|
|
84
88
|
## Supported platforms
|
|
85
89
|
- [Airbyte](https://cartography-cncf.github.io/cartography/modules/airbyte/index.html) - Organization, Workspace, User, Source, Destination, Connection, Tag, Stream
|
|
86
|
-
- [Amazon Web Services](https://cartography-cncf.github.io/cartography/modules/aws/index.html) - ACM, API Gateway, CloudWatch, CodeBuild, Config, Cognito, EC2, ECS, ECR, EFS, Elasticsearch, Elastic Kubernetes Service (EKS), DynamoDB, Glue, GuardDuty, IAM, Inspector, KMS, Lambda, RDS, Redshift, Route53, S3, Secrets Manager(Secret Versions), Security Hub, SNS, SQS, SSM, STS, Tags
|
|
90
|
+
- [Amazon Web Services](https://cartography-cncf.github.io/cartography/modules/aws/index.html) - ACM, API Gateway, CloudWatch, CodeBuild, Config, Cognito, EC2, ECS, ECR (including image layers), EFS, Elasticsearch, Elastic Kubernetes Service (EKS), DynamoDB, Glue, GuardDuty, IAM, Inspector, KMS, Lambda, RDS, Redshift, Route53, S3, Secrets Manager(Secret Versions), Security Hub, SNS, SQS, SSM, STS, Tags
|
|
87
91
|
- [Anthropic](https://cartography-cncf.github.io/cartography/modules/anthropic/index.html) - Organization, ApiKey, User, Workspace
|
|
88
92
|
- [BigFix](https://cartography-cncf.github.io/cartography/modules/bigfix/index.html) - Computers
|
|
89
93
|
- [Cloudflare](https://cartography-cncf.github.io/cartography/modules/cloudflare/index.html) - Account, Role, Member, Zone, DNSRecord
|
|
@@ -97,7 +101,7 @@ You can learn more about the story behind Cartography in our [presentation at BS
|
|
|
97
101
|
- [Keycloak](https://cartography-cncf.github.io/cartography/modules/keycloak/index.html) - Realms, Users, Groups, Roles, Scopes, Clients, IdentityProviders, Authentication Flows, Authentication Executions, Organizations, Organization Domains
|
|
98
102
|
- [Kubernetes](https://cartography-cncf.github.io/cartography/modules/kubernetes/index.html) - Cluster, Namespace, Service, Pod, Container, ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding, OIDCProvider
|
|
99
103
|
- [Lastpass](https://cartography-cncf.github.io/cartography/modules/lastpass/index.html) - users
|
|
100
|
-
- [Microsoft Azure](https://cartography-cncf.github.io/cartography/modules/azure/index.html) - App Service, CosmosDB, Functions, SQL, Storage, Virtual Machine
|
|
104
|
+
- [Microsoft Azure](https://cartography-cncf.github.io/cartography/modules/azure/index.html) - App Service, CosmosDB, Functions, Logic Apps, Resource Group, SQL, Storage, Virtual Machine
|
|
101
105
|
- [Microsoft Entra ID](https://cartography-cncf.github.io/cartography/modules/entra/index.html) - Users, Groups, Applications, OUs, App Roles, federation to AWS Identity Center
|
|
102
106
|
- [NIST CVE](https://cartography-cncf.github.io/cartography/modules/cve/index.html) - Common Vulnerabilities and Exposures (CVE) data from NIST database
|
|
103
107
|
- [Okta](https://cartography-cncf.github.io/cartography/modules/okta/index.html) - users, groups, organizations, roles, applications, factors, trusted origins, reply URIs, federation to AWS roles, federation to AWS Identity Center
|
|
@@ -144,6 +148,17 @@ When you are ready to try it in production, read [here](https://cartography-cncf
|
|
|
144
148
|
|
|
145
149
|
## Usage
|
|
146
150
|
|
|
151
|
+
### Running rules
|
|
152
|
+
|
|
153
|
+
You can check your environment against common security frameworks using the `cartography-rules` command.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
cartography-rules run all
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
See [the rules docs](https://cartography-cncf.github.io/cartography/usage/rules.html) for more detail.
|
|
160
|
+
|
|
161
|
+
|
|
147
162
|
### Querying the database directly
|
|
148
163
|
|
|
149
164
|

|