cyntrisec 0.1.7__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.
- cyntrisec/__init__.py +3 -0
- cyntrisec/__main__.py +6 -0
- cyntrisec/aws/__init__.py +6 -0
- cyntrisec/aws/collectors/__init__.py +17 -0
- cyntrisec/aws/collectors/ec2.py +30 -0
- cyntrisec/aws/collectors/iam.py +116 -0
- cyntrisec/aws/collectors/lambda_.py +45 -0
- cyntrisec/aws/collectors/network.py +70 -0
- cyntrisec/aws/collectors/rds.py +38 -0
- cyntrisec/aws/collectors/s3.py +68 -0
- cyntrisec/aws/collectors/usage.py +188 -0
- cyntrisec/aws/credentials.py +153 -0
- cyntrisec/aws/normalizers/__init__.py +17 -0
- cyntrisec/aws/normalizers/ec2.py +115 -0
- cyntrisec/aws/normalizers/iam.py +182 -0
- cyntrisec/aws/normalizers/lambda_.py +83 -0
- cyntrisec/aws/normalizers/network.py +225 -0
- cyntrisec/aws/normalizers/rds.py +130 -0
- cyntrisec/aws/normalizers/s3.py +184 -0
- cyntrisec/aws/relationship_builder.py +1359 -0
- cyntrisec/aws/scanner.py +303 -0
- cyntrisec/cli/__init__.py +5 -0
- cyntrisec/cli/analyze.py +747 -0
- cyntrisec/cli/ask.py +412 -0
- cyntrisec/cli/can.py +307 -0
- cyntrisec/cli/comply.py +226 -0
- cyntrisec/cli/cuts.py +231 -0
- cyntrisec/cli/diff.py +332 -0
- cyntrisec/cli/errors.py +105 -0
- cyntrisec/cli/explain.py +348 -0
- cyntrisec/cli/main.py +114 -0
- cyntrisec/cli/manifest.py +893 -0
- cyntrisec/cli/output.py +117 -0
- cyntrisec/cli/remediate.py +643 -0
- cyntrisec/cli/report.py +462 -0
- cyntrisec/cli/scan.py +207 -0
- cyntrisec/cli/schemas.py +391 -0
- cyntrisec/cli/serve.py +164 -0
- cyntrisec/cli/setup.py +260 -0
- cyntrisec/cli/validate.py +101 -0
- cyntrisec/cli/waste.py +323 -0
- cyntrisec/core/__init__.py +31 -0
- cyntrisec/core/business_config.py +110 -0
- cyntrisec/core/business_logic.py +131 -0
- cyntrisec/core/compliance.py +437 -0
- cyntrisec/core/cost_estimator.py +301 -0
- cyntrisec/core/cuts.py +360 -0
- cyntrisec/core/diff.py +361 -0
- cyntrisec/core/graph.py +202 -0
- cyntrisec/core/paths.py +830 -0
- cyntrisec/core/schema.py +317 -0
- cyntrisec/core/simulator.py +371 -0
- cyntrisec/core/waste.py +309 -0
- cyntrisec/mcp/__init__.py +5 -0
- cyntrisec/mcp/server.py +862 -0
- cyntrisec/storage/__init__.py +7 -0
- cyntrisec/storage/filesystem.py +344 -0
- cyntrisec/storage/memory.py +113 -0
- cyntrisec/storage/protocol.py +92 -0
- cyntrisec-0.1.7.dist-info/METADATA +672 -0
- cyntrisec-0.1.7.dist-info/RECORD +65 -0
- cyntrisec-0.1.7.dist-info/WHEEL +4 -0
- cyntrisec-0.1.7.dist-info/entry_points.txt +2 -0
- cyntrisec-0.1.7.dist-info/licenses/LICENSE +190 -0
- cyntrisec-0.1.7.dist-info/licenses/NOTICE +5 -0
cyntrisec/aws/scanner.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AWS Scanner - Orchestrate collection, normalization, and analysis.
|
|
3
|
+
|
|
4
|
+
This is the main entry point for AWS scanning.
|
|
5
|
+
No database or queue dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from cyntrisec.aws.collectors import (
|
|
16
|
+
Ec2Collector,
|
|
17
|
+
IamCollector,
|
|
18
|
+
LambdaCollector,
|
|
19
|
+
NetworkCollector,
|
|
20
|
+
RdsCollector,
|
|
21
|
+
S3Collector,
|
|
22
|
+
)
|
|
23
|
+
from cyntrisec.aws.credentials import CredentialProvider
|
|
24
|
+
from cyntrisec.aws.normalizers import (
|
|
25
|
+
Ec2Normalizer,
|
|
26
|
+
IamNormalizer,
|
|
27
|
+
LambdaNormalizer,
|
|
28
|
+
NetworkNormalizer,
|
|
29
|
+
RdsNormalizer,
|
|
30
|
+
S3Normalizer,
|
|
31
|
+
)
|
|
32
|
+
from cyntrisec.core.graph import GraphBuilder
|
|
33
|
+
from cyntrisec.core.paths import PathFinder
|
|
34
|
+
from cyntrisec.core.schema import (
|
|
35
|
+
Asset,
|
|
36
|
+
Finding,
|
|
37
|
+
Relationship,
|
|
38
|
+
Snapshot,
|
|
39
|
+
SnapshotStatus,
|
|
40
|
+
)
|
|
41
|
+
from cyntrisec.storage.protocol import StorageBackend
|
|
42
|
+
|
|
43
|
+
log = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AwsScanner:
|
|
47
|
+
"""
|
|
48
|
+
Orchestrate AWS scanning.
|
|
49
|
+
|
|
50
|
+
Coordinates:
|
|
51
|
+
1. Credential acquisition (AssumeRole)
|
|
52
|
+
2. Resource collection (EC2, IAM, S3, Lambda, RDS, Network)
|
|
53
|
+
3. Normalization to canonical schema
|
|
54
|
+
4. Graph construction
|
|
55
|
+
5. Attack path analysis
|
|
56
|
+
6. Storage of results
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
storage = FileSystemStorage()
|
|
60
|
+
scanner = AwsScanner(storage)
|
|
61
|
+
snapshot = scanner.scan(
|
|
62
|
+
role_arn="arn:aws:iam::123456789012:role/ReadOnly",
|
|
63
|
+
regions=["us-east-1", "eu-west-1"]
|
|
64
|
+
)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, storage: StorageBackend):
|
|
68
|
+
self._storage = storage
|
|
69
|
+
|
|
70
|
+
def scan(
|
|
71
|
+
self,
|
|
72
|
+
regions: Sequence[str],
|
|
73
|
+
*,
|
|
74
|
+
role_arn: str | None = None,
|
|
75
|
+
external_id: str | None = None,
|
|
76
|
+
role_session_name: str | None = None,
|
|
77
|
+
profile: str | None = None,
|
|
78
|
+
business_config: str | None = None,
|
|
79
|
+
) -> Snapshot:
|
|
80
|
+
"""
|
|
81
|
+
Run a full AWS scan.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
regions: AWS regions to scan
|
|
85
|
+
role_arn: IAM role to assume (optional - uses default creds if not provided)
|
|
86
|
+
external_id: External ID for role assumption
|
|
87
|
+
profile: AWS CLI profile for base credentials
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Snapshot with scan results
|
|
91
|
+
"""
|
|
92
|
+
datetime.utcnow()
|
|
93
|
+
start_time = time.monotonic()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if role_arn:
|
|
97
|
+
log.info("Assuming role: %s", role_arn)
|
|
98
|
+
creds = CredentialProvider(profile=profile, region=regions[0])
|
|
99
|
+
session = creds.assume_role(
|
|
100
|
+
role_arn,
|
|
101
|
+
external_id=external_id,
|
|
102
|
+
session_name=role_session_name or "cyntrisec-scan",
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
log.info("Using default AWS credentials")
|
|
106
|
+
import boto3
|
|
107
|
+
|
|
108
|
+
session = boto3.Session(profile_name=profile, region_name=regions[0])
|
|
109
|
+
|
|
110
|
+
# Get account ID
|
|
111
|
+
identity = session.client("sts").get_caller_identity()
|
|
112
|
+
account_id = identity["Account"]
|
|
113
|
+
log.info("Connected to AWS account: %s", account_id)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
# Catch-all for credential/connection errors during init
|
|
116
|
+
print(f"DEBUG: Caught exception in scanner: {type(e)} {e}")
|
|
117
|
+
raise ConnectionError(f"Failed to authenticate with AWS: {e}") from e
|
|
118
|
+
|
|
119
|
+
# 2. Initialize storage
|
|
120
|
+
scan_id = self._storage.new_scan(account_id)
|
|
121
|
+
snapshot = Snapshot(
|
|
122
|
+
aws_account_id=account_id,
|
|
123
|
+
regions=list(regions),
|
|
124
|
+
scan_params={
|
|
125
|
+
"role_arn": role_arn,
|
|
126
|
+
"regions": list(regions),
|
|
127
|
+
"business_config": business_config,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
self._storage.save_snapshot(snapshot)
|
|
131
|
+
|
|
132
|
+
# 3. Collect and normalize
|
|
133
|
+
all_assets: list[Asset] = []
|
|
134
|
+
all_relationships: list[Relationship] = []
|
|
135
|
+
all_findings: list[Finding] = []
|
|
136
|
+
collector_errors: list[dict[str, str]] = []
|
|
137
|
+
|
|
138
|
+
# Collect global resources (IAM, S3)
|
|
139
|
+
log.info("Collecting global resources (IAM, S3)...")
|
|
140
|
+
try:
|
|
141
|
+
iam_data = IamCollector(session).collect_all()
|
|
142
|
+
assets, rels, findings = IamNormalizer(snapshot_id=snapshot.id).normalize(iam_data)
|
|
143
|
+
all_assets.extend(assets)
|
|
144
|
+
all_relationships.extend(rels)
|
|
145
|
+
all_findings.extend(findings)
|
|
146
|
+
log.info(" IAM: %d assets, %d relationships", len(assets), len(rels))
|
|
147
|
+
except Exception as e:
|
|
148
|
+
log.error("Error collecting IAM: %s", e)
|
|
149
|
+
collector_errors.append({"service": "iam", "error": str(e)})
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
s3_data = S3Collector(session).collect_all()
|
|
153
|
+
assets, rels, findings = S3Normalizer(snapshot_id=snapshot.id).normalize(s3_data)
|
|
154
|
+
all_assets.extend(assets)
|
|
155
|
+
all_relationships.extend(rels)
|
|
156
|
+
all_findings.extend(findings)
|
|
157
|
+
log.info(" S3: %d assets, %d findings", len(assets), len(findings))
|
|
158
|
+
except Exception as e:
|
|
159
|
+
log.error("Error collecting S3: %s", e)
|
|
160
|
+
collector_errors.append({"service": "s3", "error": str(e)})
|
|
161
|
+
|
|
162
|
+
# Collect regional resources
|
|
163
|
+
for region in regions:
|
|
164
|
+
log.info("Scanning region: %s", region)
|
|
165
|
+
|
|
166
|
+
# EC2
|
|
167
|
+
try:
|
|
168
|
+
ec2_data = Ec2Collector(session, region).collect_all()
|
|
169
|
+
assets, rels, findings = Ec2Normalizer(
|
|
170
|
+
snapshot_id=snapshot.id,
|
|
171
|
+
region=region,
|
|
172
|
+
account_id=account_id,
|
|
173
|
+
).normalize(ec2_data)
|
|
174
|
+
all_assets.extend(assets)
|
|
175
|
+
all_relationships.extend(rels)
|
|
176
|
+
all_findings.extend(findings)
|
|
177
|
+
log.info(" EC2: %d assets", len(assets))
|
|
178
|
+
except Exception as e:
|
|
179
|
+
log.error("Error collecting EC2 in %s: %s", region, e)
|
|
180
|
+
collector_errors.append({"service": "ec2", "region": region, "error": str(e)})
|
|
181
|
+
|
|
182
|
+
# Network (VPC, subnets, security groups)
|
|
183
|
+
try:
|
|
184
|
+
network_data = NetworkCollector(session, region).collect_all()
|
|
185
|
+
assets, rels, findings = NetworkNormalizer(
|
|
186
|
+
snapshot_id=snapshot.id,
|
|
187
|
+
region=region,
|
|
188
|
+
account_id=account_id,
|
|
189
|
+
).normalize(network_data)
|
|
190
|
+
all_assets.extend(assets)
|
|
191
|
+
all_relationships.extend(rels)
|
|
192
|
+
all_findings.extend(findings)
|
|
193
|
+
log.info(" Network: %d assets, %d relationships", len(assets), len(rels))
|
|
194
|
+
except Exception as e:
|
|
195
|
+
log.error("Error collecting Network in %s: %s", region, e)
|
|
196
|
+
collector_errors.append({"service": "network", "region": region, "error": str(e)})
|
|
197
|
+
|
|
198
|
+
# Lambda
|
|
199
|
+
try:
|
|
200
|
+
lambda_data = LambdaCollector(session, region).collect_all()
|
|
201
|
+
assets, rels, findings = LambdaNormalizer(
|
|
202
|
+
snapshot_id=snapshot.id,
|
|
203
|
+
region=region,
|
|
204
|
+
account_id=account_id,
|
|
205
|
+
).normalize(lambda_data)
|
|
206
|
+
all_assets.extend(assets)
|
|
207
|
+
all_relationships.extend(rels)
|
|
208
|
+
all_findings.extend(findings)
|
|
209
|
+
log.info(" Lambda: %d assets", len(assets))
|
|
210
|
+
except Exception as e:
|
|
211
|
+
log.error("Error collecting Lambda in %s: %s", region, e)
|
|
212
|
+
collector_errors.append({"service": "lambda", "region": region, "error": str(e)})
|
|
213
|
+
|
|
214
|
+
# RDS
|
|
215
|
+
try:
|
|
216
|
+
rds_data = RdsCollector(session, region).collect_all()
|
|
217
|
+
assets, rels, findings = RdsNormalizer(
|
|
218
|
+
snapshot_id=snapshot.id,
|
|
219
|
+
region=region,
|
|
220
|
+
account_id=account_id,
|
|
221
|
+
).normalize(rds_data)
|
|
222
|
+
all_assets.extend(assets)
|
|
223
|
+
all_relationships.extend(rels)
|
|
224
|
+
all_findings.extend(findings)
|
|
225
|
+
log.info(" RDS: %d assets", len(assets))
|
|
226
|
+
except Exception as e:
|
|
227
|
+
log.error("Error collecting RDS in %s: %s", region, e)
|
|
228
|
+
collector_errors.append({"service": "rds", "region": region, "error": str(e)})
|
|
229
|
+
|
|
230
|
+
# 4. Build cross-service relationships
|
|
231
|
+
log.info("Building cross-service relationships...")
|
|
232
|
+
from cyntrisec.aws.relationship_builder import RelationshipBuilder
|
|
233
|
+
|
|
234
|
+
extra_rels = RelationshipBuilder(snapshot.id).build(all_assets)
|
|
235
|
+
all_relationships.extend(extra_rels)
|
|
236
|
+
log.info(" Added %d cross-service relationships", len(extra_rels))
|
|
237
|
+
|
|
238
|
+
# 5. Save collected data
|
|
239
|
+
self._storage.save_assets(all_assets)
|
|
240
|
+
self._storage.save_relationships(all_relationships)
|
|
241
|
+
self._storage.save_findings(all_findings)
|
|
242
|
+
|
|
243
|
+
log.info(
|
|
244
|
+
"Collection complete: %d assets, %d relationships, %d findings",
|
|
245
|
+
len(all_assets),
|
|
246
|
+
len(all_relationships),
|
|
247
|
+
len(all_findings),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# 5. Build graph
|
|
251
|
+
log.info("Building capability graph...")
|
|
252
|
+
graph = GraphBuilder().build(
|
|
253
|
+
assets=all_assets,
|
|
254
|
+
relationships=all_relationships,
|
|
255
|
+
)
|
|
256
|
+
log.info(
|
|
257
|
+
"Graph: %d nodes, %d edges",
|
|
258
|
+
graph.asset_count(),
|
|
259
|
+
graph.relationship_count(),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# 6. Find attack paths
|
|
263
|
+
log.info("Analyzing attack paths...")
|
|
264
|
+
entry_count = len(graph.entry_points())
|
|
265
|
+
target_count = len(graph.sensitive_targets())
|
|
266
|
+
log.info(" Entry points: %d, Sensitive targets: %d", entry_count, target_count)
|
|
267
|
+
|
|
268
|
+
if business_config:
|
|
269
|
+
try:
|
|
270
|
+
from cyntrisec.core.business_config import BusinessConfig
|
|
271
|
+
from cyntrisec.core.business_logic import BusinessLogicEngine
|
|
272
|
+
|
|
273
|
+
log.info("Loading business config from: %s", business_config)
|
|
274
|
+
cfg = BusinessConfig.load(business_config)
|
|
275
|
+
logic = BusinessLogicEngine(graph, cfg)
|
|
276
|
+
logic.apply_labels()
|
|
277
|
+
except Exception as e:
|
|
278
|
+
log.error("Failed to apply business config: %s", e)
|
|
279
|
+
if collector_errors is not None:
|
|
280
|
+
collector_errors.append({"service": "business_logic", "error": str(e)})
|
|
281
|
+
|
|
282
|
+
paths = PathFinder().find_paths(graph, snapshot.id)
|
|
283
|
+
self._storage.save_attack_paths(paths)
|
|
284
|
+
log.info(" Attack paths found: %d", len(paths))
|
|
285
|
+
|
|
286
|
+
# 7. Finalize snapshot
|
|
287
|
+
duration = time.monotonic() - start_time
|
|
288
|
+
if collector_errors:
|
|
289
|
+
snapshot.status = SnapshotStatus.completed_with_errors
|
|
290
|
+
snapshot.errors = collector_errors
|
|
291
|
+
else:
|
|
292
|
+
snapshot.status = SnapshotStatus.completed
|
|
293
|
+
snapshot.completed_at = datetime.utcnow()
|
|
294
|
+
snapshot.asset_count = len(all_assets)
|
|
295
|
+
snapshot.relationship_count = len(all_relationships)
|
|
296
|
+
snapshot.finding_count = len(all_findings)
|
|
297
|
+
snapshot.path_count = len(paths)
|
|
298
|
+
self._storage.save_snapshot(snapshot)
|
|
299
|
+
|
|
300
|
+
log.info("Scan complete in %.1fs", duration)
|
|
301
|
+
log.info("Results saved to: ~/.cyntrisec/scans/%s/", scan_id)
|
|
302
|
+
|
|
303
|
+
return snapshot
|