stackport 0.3.6__tar.gz → 0.3.7__tar.gz

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 (109) hide show
  1. {stackport-0.3.6/stackport.egg-info → stackport-0.3.7}/PKG-INFO +1 -1
  2. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/ec2.py +156 -0
  3. {stackport-0.3.6 → stackport-0.3.7}/pyproject.toml +1 -1
  4. {stackport-0.3.6 → stackport-0.3.7/stackport.egg-info}/PKG-INFO +1 -1
  5. {stackport-0.3.6 → stackport-0.3.7}/stackport.egg-info/SOURCES.txt +6 -6
  6. {stackport-0.3.6 → stackport-0.3.7}/tests/test_ec2_routes.py +273 -0
  7. stackport-0.3.6/ui/dist/assets/StateMachineGraph-DnspmX4c.js → stackport-0.3.7/ui/dist/assets/StateMachineGraph-C3a2WdTx.js +1 -1
  8. stackport-0.3.6/ui/dist/assets/index-CVBAWnD3.js → stackport-0.3.7/ui/dist/assets/index-B-seC0pm.js +123 -123
  9. stackport-0.3.7/ui/dist/assets/index-BZr5kWvs.css +1 -0
  10. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/index.html +2 -2
  11. stackport-0.3.6/ui/dist/assets/index-BoJK2una.css +0 -1
  12. {stackport-0.3.6 → stackport-0.3.7}/LICENSE +0 -0
  13. {stackport-0.3.6 → stackport-0.3.7}/MANIFEST.in +0 -0
  14. {stackport-0.3.6 → stackport-0.3.7}/README.md +0 -0
  15. {stackport-0.3.6 → stackport-0.3.7}/backend/__init__.py +0 -0
  16. {stackport-0.3.6 → stackport-0.3.7}/backend/aws_client.py +0 -0
  17. {stackport-0.3.6 → stackport-0.3.7}/backend/cache.py +0 -0
  18. {stackport-0.3.6 → stackport-0.3.7}/backend/cli.py +0 -0
  19. {stackport-0.3.6 → stackport-0.3.7}/backend/config.py +0 -0
  20. {stackport-0.3.6 → stackport-0.3.7}/backend/endpoint_store.py +0 -0
  21. {stackport-0.3.6 → stackport-0.3.7}/backend/main.py +0 -0
  22. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/__init__.py +0 -0
  23. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/common.py +0 -0
  24. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/dynamodb.py +0 -0
  25. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/endpoints.py +0 -0
  26. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/iam.py +0 -0
  27. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/lambda_svc.py +0 -0
  28. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/logs.py +0 -0
  29. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/rds.py +0 -0
  30. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/resources.py +0 -0
  31. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/s3.py +0 -0
  32. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/secretsmanager.py +0 -0
  33. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/sqs.py +0 -0
  34. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/stats.py +0 -0
  35. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/stepfunctions.py +0 -0
  36. {stackport-0.3.6 → stackport-0.3.7}/backend/routes/tags.py +0 -0
  37. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/__init__.py +0 -0
  38. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/dynamodb.py +0 -0
  39. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/endpoints.py +0 -0
  40. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/lambda_svc.py +0 -0
  41. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/s3.py +0 -0
  42. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/secretsmanager.py +0 -0
  43. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/sqs.py +0 -0
  44. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/stepfunctions.py +0 -0
  45. {stackport-0.3.6 → stackport-0.3.7}/backend/schemas/tags.py +0 -0
  46. {stackport-0.3.6 → stackport-0.3.7}/backend/websocket.py +0 -0
  47. {stackport-0.3.6 → stackport-0.3.7}/setup.cfg +0 -0
  48. {stackport-0.3.6 → stackport-0.3.7}/stackport.egg-info/dependency_links.txt +0 -0
  49. {stackport-0.3.6 → stackport-0.3.7}/stackport.egg-info/entry_points.txt +0 -0
  50. {stackport-0.3.6 → stackport-0.3.7}/stackport.egg-info/requires.txt +0 -0
  51. {stackport-0.3.6 → stackport-0.3.7}/stackport.egg-info/top_level.txt +0 -0
  52. {stackport-0.3.6 → stackport-0.3.7}/tests/test_cache.py +0 -0
  53. {stackport-0.3.6 → stackport-0.3.7}/tests/test_cli.py +0 -0
  54. {stackport-0.3.6 → stackport-0.3.7}/tests/test_client.py +0 -0
  55. {stackport-0.3.6 → stackport-0.3.7}/tests/test_config.py +0 -0
  56. {stackport-0.3.6 → stackport-0.3.7}/tests/test_dynamodb_routes.py +0 -0
  57. {stackport-0.3.6 → stackport-0.3.7}/tests/test_endpoint_store.py +0 -0
  58. {stackport-0.3.6 → stackport-0.3.7}/tests/test_endpoints.py +0 -0
  59. {stackport-0.3.6 → stackport-0.3.7}/tests/test_iam_routes.py +0 -0
  60. {stackport-0.3.6 → stackport-0.3.7}/tests/test_lambda_routes.py +0 -0
  61. {stackport-0.3.6 → stackport-0.3.7}/tests/test_logs_routes.py +0 -0
  62. {stackport-0.3.6 → stackport-0.3.7}/tests/test_multi_endpoint.py +0 -0
  63. {stackport-0.3.6 → stackport-0.3.7}/tests/test_rds_routes.py +0 -0
  64. {stackport-0.3.6 → stackport-0.3.7}/tests/test_readonly_middleware.py +0 -0
  65. {stackport-0.3.6 → stackport-0.3.7}/tests/test_registries.py +0 -0
  66. {stackport-0.3.6 → stackport-0.3.7}/tests/test_routes.py +0 -0
  67. {stackport-0.3.6 → stackport-0.3.7}/tests/test_s3_routes.py +0 -0
  68. {stackport-0.3.6 → stackport-0.3.7}/tests/test_s3_upload_limit_env.py +0 -0
  69. {stackport-0.3.6 → stackport-0.3.7}/tests/test_secretsmanager_routes.py +0 -0
  70. {stackport-0.3.6 → stackport-0.3.7}/tests/test_sqs_routes.py +0 -0
  71. {stackport-0.3.6 → stackport-0.3.7}/tests/test_tags_routes.py +0 -0
  72. {stackport-0.3.6 → stackport-0.3.7}/tests/test_websocket.py +0 -0
  73. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  74. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/acm.svg +0 -0
  75. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/apigateway.svg +0 -0
  76. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/appsync.svg +0 -0
  77. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/athena.svg +0 -0
  78. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/cloudformation.svg +0 -0
  79. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/cloudfront.svg +0 -0
  80. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  81. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/dynamodb.svg +0 -0
  82. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/ec2.svg +0 -0
  83. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/ecr.svg +0 -0
  84. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/ecs.svg +0 -0
  85. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/elasticache.svg +0 -0
  86. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  87. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  88. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  89. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/events.svg +0 -0
  90. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/firehose.svg +0 -0
  91. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/glue.svg +0 -0
  92. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/iam.svg +0 -0
  93. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/kinesis.svg +0 -0
  94. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/kms.svg +0 -0
  95. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/lambda.svg +0 -0
  96. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/logs.svg +0 -0
  97. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/monitoring.svg +0 -0
  98. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/rds.svg +0 -0
  99. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/route53.svg +0 -0
  100. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/s3.svg +0 -0
  101. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  102. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/ses.svg +0 -0
  103. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/sns.svg +0 -0
  104. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/sqs.svg +0 -0
  105. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/ssm.svg +0 -0
  106. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  107. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/aws-icons/wafv2.svg +0 -0
  108. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/favicon.png +0 -0
  109. {stackport-0.3.6 → stackport-0.3.7}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -301,6 +301,162 @@ def list_security_groups(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[
301
301
  raise HTTPException(status_code=500, detail=str(e))
302
302
 
303
303
 
304
+ def _get_rule_id(rule: dict, rule_type: str, idx: int) -> str:
305
+ """Generate a rule ID based on rule properties."""
306
+ return f"{rule_type}-sgrule-{idx:04d}"
307
+
308
+
309
+ def _get_ip_version(ip_range: dict | None, ipv6_range: dict | None) -> str:
310
+ """Determine IP version from rule."""
311
+ if ipv6_range:
312
+ return "IPv6"
313
+ if ip_range:
314
+ cidr = ip_range.get("CidrIp", "")
315
+ return "IPv6" if ":" in cidr else "IPv4"
316
+ return "IPv4"
317
+
318
+
319
+ def _format_port_range(protocol: str, from_port: int | None, to_port: int | None) -> str:
320
+ """Format port range for display."""
321
+ if protocol == "-1" or protocol == "all":
322
+ return "All"
323
+ if from_port is None and to_port is None:
324
+ return "All"
325
+ if from_port is not None and to_port is not None and from_port != to_port:
326
+ return f"{from_port}-{to_port}"
327
+ if from_port is not None:
328
+ return str(from_port)
329
+ return "All"
330
+
331
+
332
+ def _expand_rules(ip_permissions: list[dict], rule_type: str) -> list[dict]:
333
+ """Expand IP permissions into individual rule rows."""
334
+ expanded = []
335
+ rule_idx = 0
336
+
337
+ for perm in ip_permissions:
338
+ protocol = perm.get("IpProtocol", "tcp")
339
+ from_port = perm.get("FromPort")
340
+ to_port = perm.get("ToPort")
341
+
342
+ # Normalize protocol display
343
+ if protocol == "-1":
344
+ protocol = "All"
345
+
346
+ # Expand IPv4 ranges
347
+ for ip_range in perm.get("IpRanges", []):
348
+ expanded.append({
349
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
350
+ "ipVersion": "IPv4",
351
+ "type": rule_type.capitalize(),
352
+ "protocol": protocol,
353
+ "portRange": _format_port_range(protocol, from_port, to_port),
354
+ "source": ip_range.get("CidrIp", ""),
355
+ "description": ip_range.get("Description", ""),
356
+ })
357
+ rule_idx += 1
358
+
359
+ # Expand IPv6 ranges
360
+ for ipv6_range in perm.get("Ipv6Ranges", []):
361
+ expanded.append({
362
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
363
+ "ipVersion": "IPv6",
364
+ "type": rule_type.capitalize(),
365
+ "protocol": protocol,
366
+ "portRange": _format_port_range(protocol, from_port, to_port),
367
+ "source": ipv6_range.get("CidrIpv6", ""),
368
+ "description": ipv6_range.get("Description", ""),
369
+ })
370
+ rule_idx += 1
371
+
372
+ # Expand prefix list IDs
373
+ for prefix_list in perm.get("PrefixListIds", []):
374
+ expanded.append({
375
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
376
+ "ipVersion": "IPv4",
377
+ "type": rule_type.capitalize(),
378
+ "protocol": protocol,
379
+ "portRange": _format_port_range(protocol, from_port, to_port),
380
+ "source": prefix_list.get("PrefixListId", ""),
381
+ "description": "",
382
+ })
383
+ rule_idx += 1
384
+
385
+ # Expand user ID group pairs (security group references)
386
+ for group_pair in perm.get("UserIdGroupPairs", []):
387
+ expanded.append({
388
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
389
+ "ipVersion": "IPv4",
390
+ "type": rule_type.capitalize(),
391
+ "protocol": protocol,
392
+ "portRange": _format_port_range(protocol, from_port, to_port),
393
+ "source": group_pair.get("GroupId", "") or group_pair.get("GroupName", ""),
394
+ "description": group_pair.get("Description", ""),
395
+ })
396
+ rule_idx += 1
397
+
398
+ # If no rules found, return empty list (don't add placeholder)
399
+ return expanded
400
+
401
+
402
+ @router.get("/security-groups/{group_id}/inbound")
403
+ def get_security_group_inbound_rules(group_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
404
+ """Get inbound rules for a specific security group with detailed rule information."""
405
+ try:
406
+ client = get_client("ec2", **ep.client_kwargs())
407
+ response = client.describe_security_groups(GroupIds=[group_id])
408
+
409
+ if not response.get("SecurityGroups"):
410
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
411
+
412
+ sg = response["SecurityGroups"][0]
413
+ inbound_rules = _expand_rules(sg.get("IpPermissions", []), "inbound")
414
+
415
+ return {
416
+ "groupId": group_id,
417
+ "groupName": sg["GroupName"],
418
+ "inboundRules": inbound_rules,
419
+ }
420
+ except HTTPException:
421
+ raise
422
+ except client.exceptions.ClientError as e:
423
+ error_code = e.response["Error"]["Code"]
424
+ if error_code == "InvalidGroupId.NotFound":
425
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
426
+ raise HTTPException(status_code=500, detail=str(e))
427
+ except Exception as e:
428
+ raise HTTPException(status_code=500, detail=str(e))
429
+
430
+
431
+ @router.get("/security-groups/{group_id}/outbound")
432
+ def get_security_group_outbound_rules(group_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
433
+ """Get outbound rules for a specific security group with detailed rule information."""
434
+ try:
435
+ client = get_client("ec2", **ep.client_kwargs())
436
+ response = client.describe_security_groups(GroupIds=[group_id])
437
+
438
+ if not response.get("SecurityGroups"):
439
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
440
+
441
+ sg = response["SecurityGroups"][0]
442
+ outbound_rules = _expand_rules(sg.get("IpPermissionsEgress", []), "outbound")
443
+
444
+ return {
445
+ "groupId": group_id,
446
+ "groupName": sg["GroupName"],
447
+ "outboundRules": outbound_rules,
448
+ }
449
+ except HTTPException:
450
+ raise
451
+ except client.exceptions.ClientError as e:
452
+ error_code = e.response["Error"]["Code"]
453
+ if error_code == "InvalidGroupId.NotFound":
454
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
455
+ raise HTTPException(status_code=500, detail=str(e))
456
+ except Exception as e:
457
+ raise HTTPException(status_code=500, detail=str(e))
458
+
459
+
304
460
  @router.get("/vpcs")
305
461
  def list_vpcs(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
306
462
  """List all VPCs with their subnets."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.3.6"
3
+ version = "0.3.7"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -13,9 +13,9 @@ backend/websocket.py
13
13
  backend/../ui/dist/favicon.png
14
14
  backend/../ui/dist/favicon.svg
15
15
  backend/../ui/dist/index.html
16
- backend/../ui/dist/assets/StateMachineGraph-DnspmX4c.js
17
- backend/../ui/dist/assets/index-BoJK2una.css
18
- backend/../ui/dist/assets/index-CVBAWnD3.js
16
+ backend/../ui/dist/assets/StateMachineGraph-C3a2WdTx.js
17
+ backend/../ui/dist/assets/index-B-seC0pm.js
18
+ backend/../ui/dist/assets/index-BZr5kWvs.css
19
19
  backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
20
20
  backend/../ui/dist/aws-icons/acm.svg
21
21
  backend/../ui/dist/aws-icons/apigateway.svg
@@ -107,9 +107,9 @@ tests/test_websocket.py
107
107
  ui/dist/favicon.png
108
108
  ui/dist/favicon.svg
109
109
  ui/dist/index.html
110
- ui/dist/assets/StateMachineGraph-DnspmX4c.js
111
- ui/dist/assets/index-BoJK2una.css
112
- ui/dist/assets/index-CVBAWnD3.js
110
+ ui/dist/assets/StateMachineGraph-C3a2WdTx.js
111
+ ui/dist/assets/index-B-seC0pm.js
112
+ ui/dist/assets/index-BZr5kWvs.css
113
113
  ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
114
114
  ui/dist/aws-icons/acm.svg
115
115
  ui/dist/aws-icons/apigateway.svg
@@ -362,3 +362,276 @@ class TestListAutoscalingGroups:
362
362
  assert instance["lifecycleState"] == "InService"
363
363
  assert instance["healthStatus"] == "Healthy"
364
364
  assert instance["availabilityZone"] == "us-east-1a"
365
+
366
+
367
+ class TestGetSecurityGroupInboundRules:
368
+ @patch("backend.routes.ec2.get_client")
369
+ def test_get_security_group_inbound_rules(self, mock_get_client):
370
+ mock_ec2 = MagicMock()
371
+ mock_get_client.return_value = mock_ec2
372
+ mock_ec2.describe_security_groups.return_value = {
373
+ "SecurityGroups": [
374
+ {
375
+ "GroupId": "sg-123456",
376
+ "GroupName": "web-sg",
377
+ "Description": "Web security group",
378
+ "VpcId": "vpc-abc123",
379
+ "IpPermissions": [
380
+ {
381
+ "IpProtocol": "tcp",
382
+ "FromPort": 80,
383
+ "ToPort": 80,
384
+ "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTP access"}],
385
+ },
386
+ {
387
+ "IpProtocol": "tcp",
388
+ "FromPort": 443,
389
+ "ToPort": 443,
390
+ "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS access"}],
391
+ },
392
+ {
393
+ "IpProtocol": "tcp",
394
+ "FromPort": 22,
395
+ "ToPort": 22,
396
+ "IpRanges": [{"CidrIp": "10.0.0.0/8", "Description": "SSH access"}],
397
+ },
398
+ ],
399
+ "IpPermissionsEgress": [],
400
+ "Tags": [],
401
+ }
402
+ ]
403
+ }
404
+
405
+ resp = client.get("/api/ec2/security-groups/sg-123456/inbound")
406
+ assert resp.status_code == 200
407
+ data = resp.json()
408
+ assert data["groupId"] == "sg-123456"
409
+ assert data["groupName"] == "web-sg"
410
+ assert len(data["inboundRules"]) == 3
411
+ rule = data["inboundRules"][0]
412
+ assert rule["ruleId"].startswith("inbound-sgrule-")
413
+ assert rule["protocol"] == "tcp"
414
+ assert rule["portRange"] == "80"
415
+ assert rule["ipVersion"] == "IPv4"
416
+ assert rule["type"] == "Inbound"
417
+ assert rule["source"] == "0.0.0.0/0"
418
+ assert rule["description"] == "HTTP access"
419
+
420
+ @patch("backend.routes.ec2.get_client")
421
+ def test_get_security_group_inbound_rules_not_found(self, mock_get_client):
422
+ mock_ec2 = MagicMock()
423
+ mock_get_client.return_value = mock_ec2
424
+ mock_ec2.describe_security_groups.return_value = {"SecurityGroups": []}
425
+
426
+ resp = client.get("/api/ec2/security-groups/sg-nonexistent/inbound")
427
+ assert resp.status_code == 404
428
+
429
+
430
+ class TestGetSecurityGroupOutboundRules:
431
+ @patch("backend.routes.ec2.get_client")
432
+ def test_get_security_group_outbound_rules(self, mock_get_client):
433
+ mock_ec2 = MagicMock()
434
+ mock_get_client.return_value = mock_ec2
435
+ mock_ec2.describe_security_groups.return_value = {
436
+ "SecurityGroups": [
437
+ {
438
+ "GroupId": "sg-123456",
439
+ "GroupName": "web-sg",
440
+ "Description": "Web security group",
441
+ "VpcId": "vpc-abc123",
442
+ "IpPermissions": [],
443
+ "IpPermissionsEgress": [
444
+ {
445
+ "IpProtocol": "tcp",
446
+ "FromPort": 443,
447
+ "ToPort": 443,
448
+ "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS outbound"}],
449
+ },
450
+ {
451
+ "IpProtocol": "tcp",
452
+ "FromPort": 80,
453
+ "ToPort": 80,
454
+ "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTP outbound"}],
455
+ },
456
+ ],
457
+ "Tags": [],
458
+ }
459
+ ]
460
+ }
461
+
462
+ resp = client.get("/api/ec2/security-groups/sg-123456/outbound")
463
+ assert resp.status_code == 200
464
+ data = resp.json()
465
+ assert data["groupId"] == "sg-123456"
466
+ assert data["groupName"] == "web-sg"
467
+ assert len(data["outboundRules"]) == 2
468
+ rule = data["outboundRules"][0]
469
+ assert rule["ruleId"].startswith("outbound-sgrule-")
470
+ assert rule["protocol"] == "tcp"
471
+ assert rule["portRange"] == "443"
472
+ assert rule["ipVersion"] == "IPv4"
473
+ assert rule["type"] == "Outbound"
474
+ assert rule["source"] == "0.0.0.0/0"
475
+ assert rule["description"] == "HTTPS outbound"
476
+
477
+ @patch("backend.routes.ec2.get_client")
478
+ def test_get_security_group_outbound_rules_not_found(self, mock_get_client):
479
+ mock_ec2 = MagicMock()
480
+ mock_get_client.return_value = mock_ec2
481
+ mock_ec2.describe_security_groups.return_value = {"SecurityGroups": []}
482
+
483
+ resp = client.get("/api/ec2/security-groups/sg-nonexistent/outbound")
484
+ assert resp.status_code == 404
485
+
486
+ @patch("backend.routes.ec2.get_client")
487
+ def test_get_security_group_outbound_rules_empty(self, mock_get_client):
488
+ mock_ec2 = MagicMock()
489
+ mock_get_client.return_value = mock_ec2
490
+ mock_ec2.describe_security_groups.return_value = {
491
+ "SecurityGroups": [
492
+ {
493
+ "GroupId": "sg-123456",
494
+ "GroupName": "empty-sg",
495
+ "Description": "Empty security group",
496
+ "VpcId": "vpc-abc123",
497
+ "IpPermissions": [],
498
+ "IpPermissionsEgress": [],
499
+ "Tags": [],
500
+ }
501
+ ]
502
+ }
503
+
504
+ resp = client.get("/api/ec2/security-groups/sg-123456/outbound")
505
+ assert resp.status_code == 200
506
+ data = resp.json()
507
+ assert data["groupId"] == "sg-123456"
508
+ assert data["groupName"] == "empty-sg"
509
+ assert len(data["outboundRules"]) == 0
510
+
511
+
512
+ class TestGetSecurityGroupInboundRulesProtocolAll:
513
+ """Test handling of protocol -1 (all traffic)."""
514
+
515
+ @patch("backend.routes.ec2.get_client")
516
+ def test_get_security_group_inbound_rules_protocol_all(self, mock_get_client):
517
+ """Test that protocol -1 is converted to 'All' and port range shows 'All'."""
518
+ mock_ec2 = MagicMock()
519
+ mock_get_client.return_value = mock_ec2
520
+ mock_ec2.describe_security_groups.return_value = {
521
+ "SecurityGroups": [
522
+ {
523
+ "GroupId": "sg-123456",
524
+ "GroupName": "ecs-tasks-sg",
525
+ "Description": "ECS tasks security group",
526
+ "VpcId": "vpc-abc123",
527
+ "IpPermissions": [
528
+ {
529
+ "IpProtocol": "-1",
530
+ "FromPort": None,
531
+ "ToPort": None,
532
+ "IpRanges": [{"CidrIp": "10.1.0.0/16", "Description": "VPC traffic"}],
533
+ },
534
+ ],
535
+ "IpPermissionsEgress": [],
536
+ "Tags": [],
537
+ }
538
+ ]
539
+ }
540
+
541
+ resp = client.get("/api/ec2/security-groups/sg-123456/inbound")
542
+ assert resp.status_code == 200
543
+ data = resp.json()
544
+ assert data["groupId"] == "sg-123456"
545
+ assert data["groupName"] == "ecs-tasks-sg"
546
+ assert len(data["inboundRules"]) == 1
547
+ rule = data["inboundRules"][0]
548
+ assert rule["ruleId"].startswith("inbound-sgrule-")
549
+ assert rule["protocol"] == "All"
550
+ assert rule["portRange"] == "All"
551
+ assert rule["ipVersion"] == "IPv4"
552
+ assert rule["type"] == "Inbound"
553
+ assert rule["source"] == "10.1.0.0/16"
554
+ assert rule["description"] == "VPC traffic"
555
+
556
+
557
+ class TestGetSecurityGroupOutboundRulesProtocolAll:
558
+ """Test handling of protocol -1 (all traffic) for outbound rules."""
559
+
560
+ @patch("backend.routes.ec2.get_client")
561
+ def test_get_security_group_outbound_rules_protocol_all(self, mock_get_client):
562
+ """Test that protocol -1 is converted to 'All' and port range shows 'All' for outbound."""
563
+ mock_ec2 = MagicMock()
564
+ mock_get_client.return_value = mock_ec2
565
+ mock_ec2.describe_security_groups.return_value = {
566
+ "SecurityGroups": [
567
+ {
568
+ "GroupId": "sg-00220f1150c89205a",
569
+ "GroupName": "web-saas-development-ecs-tasks-sg",
570
+ "Description": "ECS tasks security group",
571
+ "VpcId": "vpc-abc123",
572
+ "IpPermissions": [],
573
+ "IpPermissionsEgress": [
574
+ {
575
+ "IpProtocol": "-1",
576
+ "FromPort": None,
577
+ "ToPort": None,
578
+ "IpRanges": [{"CidrIp": "0.0.0.0/0"}],
579
+ },
580
+ ],
581
+ "Tags": [],
582
+ }
583
+ ]
584
+ }
585
+
586
+ resp = client.get("/api/ec2/security-groups/sg-00220f1150c89205a/outbound")
587
+ assert resp.status_code == 200
588
+ data = resp.json()
589
+ assert data["groupId"] == "sg-00220f1150c89205a"
590
+ assert data["groupName"] == "web-saas-development-ecs-tasks-sg"
591
+ assert len(data["outboundRules"]) == 1
592
+ rule = data["outboundRules"][0]
593
+ assert rule["ruleId"].startswith("outbound-sgrule-")
594
+ assert rule["protocol"] == "All"
595
+ assert rule["portRange"] == "All"
596
+ assert rule["ipVersion"] == "IPv4"
597
+ assert rule["type"] == "Outbound"
598
+ assert rule["source"] == "0.0.0.0/0"
599
+
600
+
601
+ class TestSecurityGroupRulesPortRanges:
602
+ """Test handling of port ranges."""
603
+
604
+ @patch("backend.routes.ec2.get_client")
605
+ def test_get_security_group_port_range(self, mock_get_client):
606
+ """Test that port ranges are formatted correctly."""
607
+ mock_ec2 = MagicMock()
608
+ mock_get_client.return_value = mock_ec2
609
+ mock_ec2.describe_security_groups.return_value = {
610
+ "SecurityGroups": [
611
+ {
612
+ "GroupId": "sg-123456",
613
+ "GroupName": "port-range-sg",
614
+ "Description": "Security group with port range",
615
+ "VpcId": "vpc-abc123",
616
+ "IpPermissions": [
617
+ {
618
+ "IpProtocol": "tcp",
619
+ "FromPort": 8000,
620
+ "ToPort": 9000,
621
+ "IpRanges": [{"CidrIp": "10.0.0.0/8"}],
622
+ },
623
+ ],
624
+ "IpPermissionsEgress": [],
625
+ "Tags": [],
626
+ }
627
+ ]
628
+ }
629
+
630
+ resp = client.get("/api/ec2/security-groups/sg-123456/inbound")
631
+ assert resp.status_code == 200
632
+ data = resp.json()
633
+ assert len(data["inboundRules"]) == 1
634
+ rule = data["inboundRules"][0]
635
+ assert rule["protocol"] == "tcp"
636
+ assert rule["portRange"] == "8000-9000"
637
+ assert rule["source"] == "10.0.0.0/8"
@@ -1,4 +1,4 @@
1
- import{c as L,C as z,a as O,b as Y,G as J,j as n,r as m,B as j}from"./index-CVBAWnD3.js";import{T as B}from"./stepfunctions-graph-B6nvU4OB.js";/**
1
+ import{c as L,C as z,a as O,b as Y,G as J,j as n,r as m,B as j}from"./index-B-seC0pm.js";import{T as B}from"./stepfunctions-graph-B6nvU4OB.js";/**
2
2
  * @license lucide-react v1.7.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.