Commit f12763587f
Unsigned
Layout: unified · split
README.org +1 −1
| @@ -17,7 +17,7 @@ connection details, choose which checks to run, and watch live progress. | |||
| 17 | 17 | ||
| 18 | | Directory | Description | | 18 | | Directory | Description | |
| 19 | |----------------------+------------------------------------------------------------------------------| | 19 | |----------------------+------------------------------------------------------------------------------| |
| 20 | | =applications/aws/= | AWS IAM users, password policy, and S3 bucket analysis | | 20 | | =applications/aws/= | AWS IAM users, account/root security, password policy, S3 public access, open security groups, CloudTrail, Config, SSO | |
| 21 | | =applications/github/= | GitHub admin enumeration, org security settings, webhooks, deploy keys, secret-scanning/Dependabot alerts, audit log, branch protections, commits | | 21 | | =applications/github/= | GitHub admin enumeration, org security settings, webhooks, deploy keys, secret-scanning/Dependabot alerts, audit log, branch protections, commits | |
| 22 | | =applications/gitlab/= | GitLab group/project members, branch protections, approvals, pipelines, audit events | | 22 | | =applications/gitlab/= | GitLab group/project members, branch protections, approvals, pipelines, audit events | |
| 23 | | =databases/mongo/= | MongoDB admin enumeration | | 23 | | =databases/mongo/= | MongoDB admin enumeration | |
applications/aws/README.md +11 −2
| @@ -1,9 +1,11 @@ | |||
| 1 | > **NOTE**: Authentication uses the standard AWS credential chain (environment | 1 | > **NOTE**: Authentication uses the standard AWS credential chain (environment |
| 2 | > variables, shared config/credentials, SSO profiles, instance roles). This tool | 2 | > variables, shared config/credentials, SSO profiles, instance roles). This tool |
| 3 | > never handles access keys directly. Read-only permissions are enough — IAM | 3 | > never handles access keys directly. Read-only permissions are enough — IAM |
| 4 | > `Get*`/`List*`, S3 `s3:GetBucket*` + `s3:ListAllMyBuckets`, and for the SSO | 4 | > `Get*`/`List*`, S3 `s3:GetBucket*` + `s3:ListAllMyBuckets`, EC2 |
| 5 | > `ec2:DescribeRegions`/`DescribeSecurityGroups`, `cloudtrail:DescribeTrails` + | ||
| 6 | > `GetTrailStatus`, `config:DescribeConfigurationRecorders*`, and for the SSO | ||
| 5 | > check `sso:List*`/`sso:Describe*`, `identitystore:Describe*`, and | 7 | > check `sso:List*`/`sso:Describe*`, `identitystore:Describe*`, and |
| 6 | > `organizations:ListAccounts`. | 8 | > `organizations:ListAccounts`. The SecurityAudit managed policy covers these. |
| 7 | 9 | ||
| 8 | --- | 10 | --- |
| 9 | 11 | ||
| @@ -42,10 +44,17 @@ Creates a directory: `<out>/aws_audit_<profile>_<YYYY-MM-DD>/` | |||
| 42 | |---|---| | 44 | |---|---| |
| 43 | | `iam_users.csv` | IAM users with MFA status, access-key count/age, console password, last use | | 45 | | `iam_users.csv` | IAM users with MFA status, access-key count/age, console password, last use | |
| 44 | | `password_policy.csv` | Account IAM password policy (length, complexity, rotation, reuse) | | 46 | | `password_policy.csv` | Account IAM password policy (length, complexity, rotation, reuse) | |
| 47 | | `account_security.csv` | Account summary — root MFA, root access keys, and resource counts | | ||
| 45 | | `s3_public_access.csv` | Per-bucket Public Access Block, policy public status, and ACL public exposure | | 48 | | `s3_public_access.csv` | Per-bucket Public Access Block, policy public status, and ACL public exposure | |
| 49 | | `open_security_groups.csv` | Security-group ingress rules open to `0.0.0.0/0` or `::/0`, across all regions | | ||
| 50 | | `cloudtrail.csv` | CloudTrail trails — logging status, multi-region, log-file validation | | ||
| 51 | | `config_recorders.csv` | AWS Config recording status per region (gaps are flagged) | | ||
| 46 | | `sso_assignments.csv` | IAM Identity Center permission-set assignments per account (Identity Center + Organizations) | | 52 | | `sso_assignments.csv` | IAM Identity Center permission-set assignments per account (Identity Center + Organizations) | |
| 47 | | `summary.txt` | Row counts per section | | 53 | | `summary.txt` | Row counts per section | |
| 48 | 54 | ||
| 55 | `open_security_groups.csv` and `config_recorders.csv` scan every enabled region, | ||
| 56 | so they take longer on accounts with many regions. | ||
| 57 | |||
| 49 | Checks that aren't available (no password policy, no Identity Center instance, | 58 | Checks that aren't available (no password policy, no Identity Center instance, |
| 50 | missing permissions) are skipped with a warning; the rest still run. | 59 | missing permissions) are skipped with a warning; the rest still run. |
| 51 | 60 | ||
applications/aws/audit.py +9 −1
| @@ -31,7 +31,7 @@ import sys | |||
| 31 | from datetime import date | 31 | from datetime import date |
| 32 | 32 | ||
| 33 | import config | 33 | import config |
| 34 | from collectors import iam, s3, sso | 34 | from collectors import iam, monitoring, s3, security_groups, sso |
| 35 | from reporters import csv_reporter | 35 | from reporters import csv_reporter |
| 36 | 36 | ||
| 37 | 37 | ||
| @@ -86,7 +86,15 @@ def run(): | |||
| 86 | 86 | ||
| 87 | collect("IAM users", iam.iam_users, "iam_users.csv") | 87 | collect("IAM users", iam.iam_users, "iam_users.csv") |
| 88 | collect("Password policy", iam.password_policy, "password_policy.csv") | 88 | collect("Password policy", iam.password_policy, "password_policy.csv") |
| 89 | collect("Account security", iam.account_security, "account_security.csv") | ||
| 89 | collect("S3 public access", s3.s3_public_access, "s3_public_access.csv") | 90 | collect("S3 public access", s3.s3_public_access, "s3_public_access.csv") |
| 91 | collect( | ||
| 92 | "Open security groups", | ||
| 93 | security_groups.security_groups, | ||
| 94 | "open_security_groups.csv", | ||
| 95 | ) | ||
| 96 | collect("CloudTrail", monitoring.cloudtrail, "cloudtrail.csv") | ||
| 97 | collect("AWS Config recorders", monitoring.config_recorders, "config_recorders.csv") | ||
| 90 | collect("SSO assignments", sso.sso_assignments, "sso_assignments.csv") | 98 | collect("SSO assignments", sso.sso_assignments, "sso_assignments.csv") |
| 91 | 99 | ||
| 92 | print() | 100 | print() |
applications/aws/collectors/api.py +6
| @@ -35,3 +35,9 @@ def account_id(cfg): | |||
| 35 | return cfg["session"].client("sts").get_caller_identity()["Account"] | 35 | return cfg["session"].client("sts").get_caller_identity()["Account"] |
| 36 | except Exception: | 36 | except Exception: |
| 37 | return "" | 37 | return "" |
| 38 | |||
| 39 | |||
| 40 | def enabled_regions(cfg): | ||
| 41 | """Return the region names enabled for the account (for region-scoped checks).""" | ||
| 42 | ec2 = cfg["session"].client("ec2", region_name=cfg.get("region") or "us-east-1") | ||
| 43 | return [r["RegionName"] for r in ec2.describe_regions().get("Regions", [])] | ||
applications/aws/collectors/iam.py +25 −1
| @@ -1,5 +1,6 @@ | |||
| 1 | """ | 1 | """ |
| 2 | Collect IAM user hygiene and the account password policy. | 2 | Collect IAM user hygiene, the account password policy, and account-level |
| 3 | security summary (root MFA, root access keys). | ||
| 3 | """ | 4 | """ |
| 4 | 5 | ||
| 5 | import sys | 6 | import sys |
| @@ -8,6 +9,29 @@ from datetime import datetime, timezone | |||
| 8 | from botocore.exceptions import ClientError | 9 | from botocore.exceptions import ClientError |
| 9 | 10 | ||
| 10 | 11 | ||
| 12 | def account_security(cfg): | ||
| 13 | """ | ||
| 14 | One row of account-level security signals from the IAM account summary: | ||
| 15 | whether the root user has MFA and access keys, plus resource counts. | ||
| 16 | """ | ||
| 17 | iam = cfg["session"].client("iam") | ||
| 18 | s = iam.get_account_summary()["SummaryMap"] | ||
| 19 | return [ | ||
| 20 | { | ||
| 21 | "root_mfa_enabled": bool(s.get("AccountMFAEnabled", 0)), | ||
| 22 | "root_access_keys_present": bool(s.get("AccountAccessKeysPresent", 0)), | ||
| 23 | "root_signing_certs_present": bool( | ||
| 24 | s.get("AccountSigningCertificatesPresent", 0) | ||
| 25 | ), | ||
| 26 | "mfa_devices": s.get("MFADevices", 0), | ||
| 27 | "users": s.get("Users", 0), | ||
| 28 | "groups": s.get("Groups", 0), | ||
| 29 | "roles": s.get("Roles", 0), | ||
| 30 | "policies": s.get("Policies", 0), | ||
| 31 | } | ||
| 32 | ] | ||
| 33 | |||
| 34 | |||
| 11 | def iam_users(cfg): | 35 | def iam_users(cfg): |
| 12 | """ | 36 | """ |
| 13 | One row per IAM user: MFA status, access-key count and oldest key age, | 37 | One row per IAM user: MFA status, access-key count and oldest key age, |
applications/aws/collectors/monitoring.py added +81
| @@ -0,0 +1,81 @@ | |||
| 1 | """ | ||
| 2 | Collect audit-logging posture: CloudTrail trails and AWS Config recorders. | ||
| 3 | """ | ||
| 4 | |||
| 5 | import sys | ||
| 6 | |||
| 7 | from botocore.exceptions import ClientError | ||
| 8 | |||
| 9 | from .api import enabled_regions | ||
| 10 | |||
| 11 | |||
| 12 | def cloudtrail(cfg): | ||
| 13 | """ | ||
| 14 | One row per CloudTrail trail: whether it is logging, multi-region, and has | ||
| 15 | log-file validation. An empty result means no trails are configured. | ||
| 16 | """ | ||
| 17 | region = cfg.get("region") or "us-east-1" | ||
| 18 | ct = cfg["session"].client("cloudtrail", region_name=region) | ||
| 19 | rows = [] | ||
| 20 | for trail in ct.describe_trails(includeShadowTrails=False).get("trailList", []): | ||
| 21 | try: | ||
| 22 | status = ct.get_trail_status(Name=trail["TrailARN"]) | ||
| 23 | except ClientError: | ||
| 24 | status = {} | ||
| 25 | rows.append( | ||
| 26 | { | ||
| 27 | "name": trail.get("Name", ""), | ||
| 28 | "home_region": trail.get("HomeRegion", ""), | ||
| 29 | "multi_region": trail.get("IsMultiRegionTrail"), | ||
| 30 | "log_file_validation": trail.get("LogFileValidationEnabled"), | ||
| 31 | "is_logging": status.get("IsLogging"), | ||
| 32 | "s3_bucket": trail.get("S3BucketName", ""), | ||
| 33 | } | ||
| 34 | ) | ||
| 35 | return rows | ||
| 36 | |||
| 37 | |||
| 38 | def config_recorders(cfg): | ||
| 39 | """ | ||
| 40 | One row per region: whether AWS Config is recording. Regions with no | ||
| 41 | recorder are reported so gaps are visible. | ||
| 42 | """ | ||
| 43 | session = cfg["session"] | ||
| 44 | rows = [] | ||
| 45 | for region in enabled_regions(cfg): | ||
| 46 | try: | ||
| 47 | cc = session.client("config", region_name=region) | ||
| 48 | recorders = cc.describe_configuration_recorders().get( | ||
| 49 | "ConfigurationRecorders", [] | ||
| 50 | ) | ||
| 51 | statuses = { | ||
| 52 | s["name"]: s | ||
| 53 | for s in cc.describe_configuration_recorder_status().get( | ||
| 54 | "ConfigurationRecordersStatus", [] | ||
| 55 | ) | ||
| 56 | } | ||
| 57 | except ClientError as e: | ||
| 58 | print(f" Skipping {region}: config returned {e}", file=sys.stderr) | ||
| 59 | continue | ||
| 60 | |||
| 61 | if not recorders: | ||
| 62 | rows.append( | ||
| 63 | { | ||
| 64 | "region": region, | ||
| 65 | "recorder": "(none)", | ||
| 66 | "recording": False, | ||
| 67 | "last_status": "", | ||
| 68 | } | ||
| 69 | ) | ||
| 70 | continue | ||
| 71 | for r in recorders: | ||
| 72 | st = statuses.get(r["name"], {}) | ||
| 73 | rows.append( | ||
| 74 | { | ||
| 75 | "region": region, | ||
| 76 | "recorder": r["name"], | ||
| 77 | "recording": st.get("recording"), | ||
| 78 | "last_status": st.get("lastStatus", ""), | ||
| 79 | } | ||
| 80 | ) | ||
| 81 | return rows | ||
applications/aws/collectors/security_groups.py added +63
| @@ -0,0 +1,63 @@ | |||
| 1 | """ | ||
| 2 | Collect security-group ingress rules open to the internet, across all regions. | ||
| 3 | |||
| 4 | Only rules allowing 0.0.0.0/0 or ::/0 are reported — one row per open rule. | ||
| 5 | """ | ||
| 6 | |||
| 7 | import sys | ||
| 8 | |||
| 9 | from botocore.exceptions import ClientError | ||
| 10 | |||
| 11 | from .api import enabled_regions | ||
| 12 | |||
| 13 | OPEN_V4 = "0.0.0.0/0" | ||
| 14 | OPEN_V6 = "::/0" | ||
| 15 | |||
| 16 | |||
| 17 | def security_groups(cfg): | ||
| 18 | session = cfg["session"] | ||
| 19 | rows = [] | ||
| 20 | for region in enabled_regions(cfg): | ||
| 21 | try: | ||
| 22 | ec2 = session.client("ec2", region_name=region) | ||
| 23 | groups = _all_groups(ec2) | ||
| 24 | except ClientError as e: | ||
| 25 | print(f" Skipping {region}: ec2 returned {e}", file=sys.stderr) | ||
| 26 | continue | ||
| 27 | for sg in groups: | ||
| 28 | rows.extend(_open_rules(region, sg)) | ||
| 29 | return rows | ||
| 30 | |||
| 31 | |||
| 32 | def _all_groups(ec2): | ||
| 33 | groups = [] | ||
| 34 | for page in ec2.get_paginator("describe_security_groups").paginate(): | ||
| 35 | groups.extend(page.get("SecurityGroups", [])) | ||
| 36 | return groups | ||
| 37 | |||
| 38 | |||
| 39 | def _open_rules(region, sg): | ||
| 40 | rows = [] | ||
| 41 | for perm in sg.get("IpPermissions", []): | ||
| 42 | open_to = [ | ||
| 43 | r["CidrIp"] for r in perm.get("IpRanges", []) if r.get("CidrIp") == OPEN_V4 | ||
| 44 | ] | ||
| 45 | open_to += [ | ||
| 46 | r["CidrIpv6"] | ||
| 47 | for r in perm.get("Ipv6Ranges", []) | ||
| 48 | if r.get("CidrIpv6") == OPEN_V6 | ||
| 49 | ] | ||
| 50 | if not open_to: | ||
| 51 | continue | ||
| 52 | rows.append( | ||
| 53 | { | ||
| 54 | "region": region, | ||
| 55 | "group_id": sg.get("GroupId", ""), | ||
| 56 | "group_name": sg.get("GroupName", ""), | ||
| 57 | "protocol": perm.get("IpProtocol", ""), | ||
| 58 | "from_port": perm.get("FromPort", "all"), | ||
| 59 | "to_port": perm.get("ToPort", "all"), | ||
| 60 | "open_to": ", ".join(open_to), | ||
| 61 | } | ||
| 62 | ) | ||
| 63 | return rows | ||
tui/aws_runner.py +29 −1
| @@ -21,7 +21,14 @@ _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||
| 21 | if _REPO_ROOT not in sys.path: | 21 | if _REPO_ROOT not in sys.path: |
| 22 | sys.path.insert(0, _REPO_ROOT) | 22 | sys.path.insert(0, _REPO_ROOT) |
| 23 | 23 | ||
| 24 | from applications.aws.collectors import api, iam, s3, sso | 24 | from applications.aws.collectors import ( |
| 25 | api, | ||
| 26 | iam, | ||
| 27 | monitoring, | ||
| 28 | s3, | ||
| 29 | security_groups, | ||
| 30 | sso, | ||
| 31 | ) | ||
| 25 | from applications.aws.reporters import csv_reporter | 32 | from applications.aws.reporters import csv_reporter |
| 26 | 33 | ||
| 27 | # --- Check registry --------------------------------------------------------- | 34 | # --- Check registry --------------------------------------------------------- |
| @@ -34,12 +41,33 @@ CHECKS: list[Check] = [ | |||
| 34 | iam.password_policy, | 41 | iam.password_policy, |
| 35 | "password_policy.csv", | 42 | "password_policy.csv", |
| 36 | ), | 43 | ), |
| 44 | Check( | ||
| 45 | "account_security", | ||
| 46 | "Account security (root MFA)", | ||
| 47 | iam.account_security, | ||
| 48 | "account_security.csv", | ||
| 49 | ), | ||
| 37 | Check( | 50 | Check( |
| 38 | "s3_public_access", | 51 | "s3_public_access", |
| 39 | "S3 public access", | 52 | "S3 public access", |
| 40 | s3.s3_public_access, | 53 | s3.s3_public_access, |
| 41 | "s3_public_access.csv", | 54 | "s3_public_access.csv", |
| 42 | ), | 55 | ), |
| 56 | Check( | ||
| 57 | "security_groups", | ||
| 58 | "Open security groups", | ||
| 59 | security_groups.security_groups, | ||
| 60 | "open_security_groups.csv", | ||
| 61 | note="scans all regions", | ||
| 62 | ), | ||
| 63 | Check("cloudtrail", "CloudTrail", monitoring.cloudtrail, "cloudtrail.csv"), | ||
| 64 | Check( | ||
| 65 | "config_recorders", | ||
| 66 | "AWS Config recorders", | ||
| 67 | monitoring.config_recorders, | ||
| 68 | "config_recorders.csv", | ||
| 69 | note="scans all regions", | ||
| 70 | ), | ||
| 43 | Check( | 71 | Check( |
| 44 | "sso_assignments", | 72 | "sso_assignments", |
| 45 | "SSO assignments", | 73 | "SSO assignments", |
tui/tests/test_aws_collectors.py added +166
| @@ -0,0 +1,166 @@ | |||
| 1 | """Unit tests for the new AWS security collectors, mocking boto3 clients.""" | ||
| 2 | |||
| 3 | from unittest.mock import MagicMock | ||
| 4 | |||
| 5 | from applications.aws.collectors import iam as aws_iam | ||
| 6 | from applications.aws.collectors import monitoring | ||
| 7 | from applications.aws.collectors import security_groups as sg | ||
| 8 | |||
| 9 | |||
| 10 | class FakeSession: | ||
| 11 | """Dispatch .client(service, region_name=...) to preconfigured mocks.""" | ||
| 12 | |||
| 13 | def __init__(self, clients): | ||
| 14 | self._clients = clients | ||
| 15 | |||
| 16 | def client(self, service, region_name=None): | ||
| 17 | return self._clients[service] | ||
| 18 | |||
| 19 | |||
| 20 | def _cfg(clients): | ||
| 21 | return {"session": FakeSession(clients), "region": "us-east-1"} | ||
| 22 | |||
| 23 | |||
| 24 | # --- account_security ------------------------------------------------------- | ||
| 25 | |||
| 26 | |||
| 27 | def test_account_security(): | ||
| 28 | iam = MagicMock() | ||
| 29 | iam.get_account_summary.return_value = { | ||
| 30 | "SummaryMap": { | ||
| 31 | "AccountMFAEnabled": 1, | ||
| 32 | "AccountAccessKeysPresent": 0, | ||
| 33 | "Users": 5, | ||
| 34 | "Roles": 12, | ||
| 35 | } | ||
| 36 | } | ||
| 37 | rows = aws_iam.account_security(_cfg({"iam": iam})) | ||
| 38 | assert rows[0]["root_mfa_enabled"] is True | ||
| 39 | assert rows[0]["root_access_keys_present"] is False | ||
| 40 | assert rows[0]["users"] == 5 | ||
| 41 | assert rows[0]["roles"] == 12 | ||
| 42 | |||
| 43 | |||
| 44 | # --- cloudtrail ------------------------------------------------------------- | ||
| 45 | |||
| 46 | |||
| 47 | def test_cloudtrail(): | ||
| 48 | ct = MagicMock() | ||
| 49 | ct.describe_trails.return_value = { | ||
| 50 | "trailList": [ | ||
| 51 | { | ||
| 52 | "Name": "org-trail", | ||
| 53 | "TrailARN": "arn:aws:cloudtrail:...:trail/org-trail", | ||
| 54 | "HomeRegion": "us-east-1", | ||
| 55 | "IsMultiRegionTrail": True, | ||
| 56 | "LogFileValidationEnabled": True, | ||
| 57 | "S3BucketName": "logs", | ||
| 58 | } | ||
| 59 | ] | ||
| 60 | } | ||
| 61 | ct.get_trail_status.return_value = {"IsLogging": True} | ||
| 62 | rows = monitoring.cloudtrail(_cfg({"cloudtrail": ct})) | ||
| 63 | assert rows[0]["is_logging"] is True | ||
| 64 | assert rows[0]["multi_region"] is True | ||
| 65 | assert rows[0]["s3_bucket"] == "logs" | ||
| 66 | |||
| 67 | |||
| 68 | # --- config_recorders ------------------------------------------------------- | ||
| 69 | |||
| 70 | |||
| 71 | def _ec2_one_region(): | ||
| 72 | ec2 = MagicMock() | ||
| 73 | ec2.describe_regions.return_value = {"Regions": [{"RegionName": "us-east-1"}]} | ||
| 74 | return ec2 | ||
| 75 | |||
| 76 | |||
| 77 | def test_config_recorders_recording(): | ||
| 78 | config = MagicMock() | ||
| 79 | config.describe_configuration_recorders.return_value = { | ||
| 80 | "ConfigurationRecorders": [{"name": "default"}] | ||
| 81 | } | ||
| 82 | config.describe_configuration_recorder_status.return_value = { | ||
| 83 | "ConfigurationRecordersStatus": [ | ||
| 84 | {"name": "default", "recording": True, "lastStatus": "SUCCESS"} | ||
| 85 | ] | ||
| 86 | } | ||
| 87 | rows = monitoring.config_recorders( | ||
| 88 | _cfg({"ec2": _ec2_one_region(), "config": config}) | ||
| 89 | ) | ||
| 90 | assert rows == [ | ||
| 91 | { | ||
| 92 | "region": "us-east-1", | ||
| 93 | "recorder": "default", | ||
| 94 | "recording": True, | ||
| 95 | "last_status": "SUCCESS", | ||
| 96 | } | ||
| 97 | ] | ||
| 98 | |||
| 99 | |||
| 100 | def test_config_recorders_reports_gap(): | ||
| 101 | config = MagicMock() | ||
| 102 | config.describe_configuration_recorders.return_value = { | ||
| 103 | "ConfigurationRecorders": [] | ||
| 104 | } | ||
| 105 | config.describe_configuration_recorder_status.return_value = { | ||
| 106 | "ConfigurationRecordersStatus": [] | ||
| 107 | } | ||
| 108 | rows = monitoring.config_recorders( | ||
| 109 | _cfg({"ec2": _ec2_one_region(), "config": config}) | ||
| 110 | ) | ||
| 111 | assert rows[0]["recorder"] == "(none)" | ||
| 112 | assert rows[0]["recording"] is False | ||
| 113 | |||
| 114 | |||
| 115 | # --- security_groups -------------------------------------------------------- | ||
| 116 | |||
| 117 | |||
| 118 | def _ec2_with_groups(groups): | ||
| 119 | ec2 = _ec2_one_region() | ||
| 120 | paginator = MagicMock() | ||
| 121 | paginator.paginate.return_value = [{"SecurityGroups": groups}] | ||
| 122 | ec2.get_paginator.return_value = paginator | ||
| 123 | return ec2 | ||
| 124 | |||
| 125 | |||
| 126 | def test_security_groups_flags_open_ingress(): | ||
| 127 | groups = [ | ||
| 128 | { | ||
| 129 | "GroupId": "sg-1", | ||
| 130 | "GroupName": "web", | ||
| 131 | "IpPermissions": [ | ||
| 132 | { | ||
| 133 | "IpProtocol": "tcp", | ||
| 134 | "FromPort": 22, | ||
| 135 | "ToPort": 22, | ||
| 136 | "IpRanges": [{"CidrIp": "0.0.0.0/0"}], | ||
| 137 | "Ipv6Ranges": [], | ||
| 138 | } | ||
| 139 | ], | ||
| 140 | } | ||
| 141 | ] | ||
| 142 | rows = sg.security_groups(_cfg({"ec2": _ec2_with_groups(groups)})) | ||
| 143 | assert len(rows) == 1 | ||
| 144 | assert rows[0]["group_id"] == "sg-1" | ||
| 145 | assert rows[0]["from_port"] == 22 | ||
| 146 | assert rows[0]["open_to"] == "0.0.0.0/0" | ||
| 147 | |||
| 148 | |||
| 149 | def test_security_groups_ignores_scoped_ingress(): | ||
| 150 | groups = [ | ||
| 151 | { | ||
| 152 | "GroupId": "sg-2", | ||
| 153 | "GroupName": "internal", | ||
| 154 | "IpPermissions": [ | ||
| 155 | { | ||
| 156 | "IpProtocol": "tcp", | ||
| 157 | "FromPort": 5432, | ||
| 158 | "ToPort": 5432, | ||
| 159 | "IpRanges": [{"CidrIp": "10.0.0.0/8"}], | ||
| 160 | "Ipv6Ranges": [], | ||
| 161 | } | ||
| 162 | ], | ||
| 163 | } | ||
| 164 | ] | ||
| 165 | rows = sg.security_groups(_cfg({"ec2": _ec2_with_groups(groups)})) | ||
| 166 | assert rows == [] | ||