nate2-scripts 1.0.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.
@@ -0,0 +1,178 @@
1
+ import re
2
+ import shutil
3
+ import sqlite3
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+
10
+ SUPPORTED_BROWSERS = {
11
+ "com.google.chrome": "chrome",
12
+ "org.mozilla.firefox": "firefox",
13
+ }
14
+
15
+ WAIT_SECONDS = 10 # Time to wait for the browser to open and record the hash
16
+
17
+ ONLY_HASH_SECONDS = 5 # Time to wait for the browser to open and record the hash, but only return the hash if found
18
+
19
+
20
+ class HashGrabber:
21
+
22
+ @staticmethod
23
+ def _get_default_browser() -> str:
24
+ """Return 'chrome' or 'firefox'. Prints to stderr and exits for unsupported browsers."""
25
+ result = subprocess.run(
26
+ [
27
+ "defaults", "read",
28
+ str(Path.home() / "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure"),
29
+ "LSHandlers",
30
+ ],
31
+ capture_output=True,
32
+ text=True,
33
+ )
34
+ # Find the block containing LSHandlerURLScheme = https and extract LSHandlerRoleAll
35
+ match = re.search(
36
+ r'LSHandlerRoleAll\s*=\s*"([^"]+)";\s*LSHandlerURLScheme\s*=\s*https;',
37
+ result.stdout,
38
+ )
39
+ bundle_id = match.group(1).lower() if match else ""
40
+
41
+ if bundle_id in SUPPORTED_BROWSERS:
42
+ return SUPPORTED_BROWSERS[bundle_id]
43
+
44
+ print(
45
+ f"Unsupported default browser: {bundle_id or 'unknown'}. Only Chrome and Firefox are supported.",
46
+ file=sys.stderr,
47
+ )
48
+ sys.exit(1)
49
+
50
+ @staticmethod
51
+ def get_hash(id: str, time_started: float) -> str:
52
+ """Search the default browser's history for the most recent AWS console URL matching the account ID.
53
+
54
+ Polls up to WAIT_SECONDS for a URL visited after time_started.
55
+ Returns the hash portion from URLs like https://{id}-{hash}.{region}.console.aws.amazon.com/*
56
+ or empty string for URLs like https://{region}.console.aws.amazon.com/*.
57
+
58
+ Raises RuntimeError if no matching URL is found within the timeout.
59
+ """
60
+ default = HashGrabber._get_default_browser()
61
+
62
+ pattern_with_hash = re.compile(
63
+ rf"https://{re.escape(id)}-([a-z0-9]+)\.[a-z0-9-]+\.console\.aws\.amazon\.com"
64
+ )
65
+ pattern_without_hash = re.compile(
66
+ r"https://[a-z0-9-]+\.console\.aws\.amazon\.com"
67
+ )
68
+
69
+ fetch_fn = {
70
+ "chrome": HashGrabber._chrome_history,
71
+ "firefox": HashGrabber._firefox_history,
72
+ }[default]
73
+
74
+ deadline = time_started + WAIT_SECONDS
75
+ hash_only_deadline = time_started + ONLY_HASH_SECONDS
76
+ time.sleep(0.5)
77
+
78
+ while time.time() < deadline:
79
+ try:
80
+ print("-----------------------------------------")
81
+ for url, visit_time in fetch_fn(id, time_started):
82
+ print(url, visit_time)
83
+ match = pattern_with_hash.search(url)
84
+ if match:
85
+ return match.group(1)
86
+ elif time.time() >= hash_only_deadline and pattern_without_hash.search(url):
87
+ return ""
88
+ except (OSError, sqlite3.Error):
89
+ pass
90
+
91
+ time.sleep(0.5)
92
+
93
+ raise RuntimeError(
94
+ f"No AWS console URL found for account {id} within {WAIT_SECONDS}s"
95
+ )
96
+
97
+ @staticmethod
98
+ def _chrome_history(account_id: str, time_started: float) -> list[tuple[str, float]]:
99
+ db_path = HashGrabber._chrome_sqlite_path()
100
+ if not db_path.exists():
101
+ return []
102
+
103
+ # Chrome stores time as microseconds since 1601-01-01
104
+ chrome_epoch_offset = 11644473600
105
+ chrome_time_started = int((time_started + chrome_epoch_offset) * 1_000_000)
106
+
107
+ with tempfile.NamedTemporaryFile(suffix=".sqlite") as tmp:
108
+ shutil.copy2(db_path, tmp.name)
109
+ conn = sqlite3.connect(tmp.name)
110
+ try:
111
+ cursor = conn.execute(
112
+ """
113
+ SELECT url, last_visit_time
114
+ FROM urls
115
+ WHERE (url LIKE ? OR url LIKE '%console.aws.amazon.com%')
116
+ AND last_visit_time > ?
117
+ ORDER BY last_visit_time DESC
118
+ LIMIT 10
119
+ """,
120
+ (f"%{account_id}%console.aws.amazon.com%", chrome_time_started),
121
+ )
122
+ return cursor.fetchall()
123
+ finally:
124
+ conn.close()
125
+
126
+ @staticmethod
127
+ def _firefox_history(account_id: str, time_started: float) -> list[tuple[str, float]]:
128
+ db_path = HashGrabber._firefox_sqlite_path()
129
+ if not db_path or not db_path.exists():
130
+ return []
131
+
132
+ # Firefox stores time as microseconds since Unix epoch
133
+ firefox_time_started = int(time_started * 1_000_000)
134
+
135
+ with tempfile.NamedTemporaryFile(suffix=".sqlite") as tmp:
136
+ shutil.copy2(db_path, tmp.name)
137
+ conn = sqlite3.connect(tmp.name)
138
+ try:
139
+ cursor = conn.execute(
140
+ """
141
+ SELECT p.url, v.visit_date
142
+ FROM moz_places p
143
+ JOIN moz_historyvisits v ON v.place_id = p.id
144
+ WHERE (p.url LIKE ? OR p.url LIKE '%console.aws.amazon.com%')
145
+ AND v.visit_date > ?
146
+ ORDER BY v.visit_date DESC
147
+ LIMIT 10
148
+ """,
149
+ (f"%{account_id}%console.aws.amazon.com%", firefox_time_started),
150
+ )
151
+ return cursor.fetchall()
152
+ finally:
153
+ conn.close()
154
+
155
+ @staticmethod
156
+ def _chrome_sqlite_path() -> Path:
157
+ return Path.home() / "Library/Application Support/Google/Chrome/Default/History"
158
+
159
+ @staticmethod
160
+ def _firefox_sqlite_path() -> Path | None:
161
+ profiles_dir = Path.home() / "Library/Application Support/Firefox/Profiles"
162
+ if not profiles_dir.exists():
163
+ return None
164
+
165
+ for pattern in ["*.default-release", "*.default"]:
166
+ matches = list(profiles_dir.glob(pattern))
167
+ if matches:
168
+ db = matches[0] / "places.sqlite"
169
+ if db.exists():
170
+ return db
171
+
172
+ for profile in profiles_dir.iterdir():
173
+ if profile.is_dir():
174
+ db = profile / "places.sqlite"
175
+ if db.exists():
176
+ return db
177
+
178
+ return None
@@ -0,0 +1,50 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+ DEFAULT_REGION = "us-east-1"
6
+ DEFAULT_REGIONS = ["us-east-1", "us-east-2", "us-west-1", "us-west-2"]
7
+
8
+
9
+ class RegionCache:
10
+ """Loads and persists the list of AWS regions and the default region.
11
+
12
+ Configuration is read from XDG_STATE_HOME/console-regions.json
13
+ (defaulting to ~/.local/state/console-regions.json). If the file is
14
+ absent or invalid it is recreated with the built-in defaults.
15
+ """
16
+
17
+ # Resolve the config file path from XDG_STATE_HOME or the default ~/.local/state
18
+ PATH = (
19
+ Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
20
+ / "console-regions.json"
21
+ )
22
+
23
+ # Class-level defaults; overwritten per-instance when the config file is loaded
24
+ regions: list[str] = DEFAULT_REGIONS
25
+ default: str = DEFAULT_REGION
26
+
27
+ def __init__(self):
28
+ """Load region config from disk, falling back to defaults if unavailable."""
29
+ if self.PATH.exists():
30
+ try:
31
+ file = json.loads(self.PATH.read_text())
32
+ if file.get("regions"):
33
+ self.regions = file["regions"]
34
+ if file.get("default"):
35
+ self.default = file["default"]
36
+ return
37
+ except (json.JSONDecodeError, OSError):
38
+ pass
39
+
40
+ # File is missing or corrupt — write defaults so subsequent reads succeed
41
+ self.PATH.parent.mkdir(parents=True, exist_ok=True)
42
+ self.PATH.write_text(json.dumps({"regions": DEFAULT_REGIONS, "default": DEFAULT_REGION}))
43
+
44
+ def get_regions(self) -> list[str]:
45
+ """Return the list of configured AWS regions."""
46
+ return self.regions
47
+
48
+ def get_default_region(self) -> str:
49
+ """Return the default AWS region."""
50
+ return self.default
@@ -0,0 +1,174 @@
1
+ import re
2
+ import sys
3
+ from typing import NoReturn
4
+ import boto3
5
+ from botocore.exceptions import ClientError
6
+
7
+ from nate2_scripts.console.regionCache import RegionCache
8
+
9
+
10
+ class ResourceIdResolver:
11
+
12
+ region_cache: RegionCache
13
+ mdn: str
14
+
15
+ def __init__(self, region_cache: RegionCache, multisession_domain_name: str):
16
+ self.region_cache = region_cache
17
+ self.mdn = multisession_domain_name
18
+
19
+ _KMS_UUID = re.compile(
20
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
21
+ )
22
+ _KMS_MRK = re.compile(r"^mrk-[0-9a-f]{32}$")
23
+
24
+ def try_parse_resource_id(self, resource_id: str) -> str | None:
25
+ kind = self._kind(resource_id)
26
+ if kind is None:
27
+ return None
28
+
29
+ # IAM resources are global — no region search needed
30
+ if kind == "role":
31
+ name = resource_id[5:]
32
+ try:
33
+ boto3.client("iam").get_role(RoleName=name)
34
+ except ClientError:
35
+ self._not_found(resource_id)
36
+ return f"https://{self.mdn}us-east-1.console.aws.amazon.com/iam/home#/roles/{name}"
37
+ if kind == "user":
38
+ name = resource_id[4:]
39
+ try:
40
+ boto3.client("iam").get_user(UserName=name)
41
+ except ClientError:
42
+ self._not_found(resource_id)
43
+ return f"https://{self.mdn}us-east-1.console.aws.amazon.com/iam/home#/users/{name}"
44
+
45
+ # regional resources — find the region that actually holds the resource
46
+ for region in self.region_cache.get_regions():
47
+ url = self._resolve_in_region(kind, resource_id, region)
48
+ if url:
49
+ return url
50
+
51
+ self._not_found(resource_id)
52
+
53
+ def _kind(self, resource_id: str) -> str | None:
54
+ if re.match(r"^i-[0-9a-f]{8,17}$", resource_id):
55
+ return "instance"
56
+ if re.match(r"^vpc-[0-9a-f]{8,17}$", resource_id):
57
+ return "vpc"
58
+ if re.match(r"^subnet-[0-9a-f]{8,17}$", resource_id):
59
+ return "subnet"
60
+ if re.match(r"^sg-[0-9a-f]{8,17}$", resource_id):
61
+ return "sg"
62
+ if re.match(r"^ami-[0-9a-f]{8,17}$", resource_id):
63
+ return "ami"
64
+ if re.match(r"^lt-[0-9a-f]{8,17}$", resource_id):
65
+ return "lt"
66
+ if re.match(r"^rtb-[0-9a-f]{8,17}$", resource_id):
67
+ return "rtb"
68
+ if re.match(r"^vol-[0-9a-f]{8,17}$", resource_id):
69
+ return "vol"
70
+ if re.match(r"^snap-[0-9a-f]{8,17}$", resource_id):
71
+ return "snap"
72
+ if re.match(r"^eni-[0-9a-f]{8,17}$", resource_id):
73
+ return "eni"
74
+ if re.match(r"^nat-[0-9a-f]{8,17}$", resource_id):
75
+ return "nat"
76
+ # no idea where this is supposed to go, but it's a thing that exists
77
+ # keeping it here in case I figure out later
78
+ # if re.match(r"^fl-[0-9a-f]{8,17}$", resource_id):
79
+ # return "fl"
80
+ if self._KMS_UUID.match(resource_id) or self._KMS_MRK.match(resource_id):
81
+ return "kms"
82
+ if resource_id.startswith("db/"):
83
+ return "rds"
84
+ if resource_id.startswith("role/"):
85
+ return "role"
86
+ if resource_id.startswith("usr/"):
87
+ return "user"
88
+ return None
89
+
90
+ def _resolve_in_region(
91
+ self, kind: str, resource_id: str, region: str
92
+ ) -> str | None:
93
+ base = f"https://{self.mdn}{region}.console.aws.amazon.com"
94
+ try:
95
+ match kind:
96
+ case "instance":
97
+ ec2 = boto3.client("ec2", region_name=region)
98
+ if ec2.describe_instances(InstanceIds=[resource_id])["Reservations"]:
99
+ return f"{base}/ec2/v2/home?region={region}#InstanceDetails:instanceId={resource_id}"
100
+ case "vpc":
101
+ ec2 = boto3.client("ec2", region_name=region)
102
+ if ec2.describe_vpcs(VpcIds=[resource_id])["Vpcs"]:
103
+ return f"{base}/vpcconsole/home?region={region}#VpcDetails:VpcId={resource_id}"
104
+ case "subnet":
105
+ ec2 = boto3.client("ec2", region_name=region)
106
+ if ec2.describe_subnets(SubnetIds=[resource_id])["Subnets"]:
107
+ return f"{base}/vpcconsole/home?region={region}#SubnetDetails:subnetId={resource_id}"
108
+ case "sg":
109
+ ec2 = boto3.client("ec2", region_name=region)
110
+ if ec2.describe_security_groups(GroupIds=[resource_id])[
111
+ "SecurityGroups"
112
+ ]:
113
+ return f"{base}/ec2/v2/home?region={region}#SecurityGroup:groupId={resource_id}"
114
+ case "ami":
115
+ ec2 = boto3.client("ec2", region_name=region)
116
+ if ec2.describe_images(ImageIds=[resource_id])["Images"]:
117
+ return f"{base}/ec2/home?region={region}#ImageDetails:imageId={resource_id}"
118
+ case "lt":
119
+ ec2 = boto3.client("ec2", region_name=region)
120
+ if ec2.describe_launch_templates(LaunchTemplateIds=[resource_id])[
121
+ "LaunchTemplates"
122
+ ]:
123
+ return f"{base}/ec2/home?region={region}#LaunchTemplateDetails:launchTemplateId={resource_id}"
124
+ case "rtb":
125
+ ec2 = boto3.client("ec2", region_name=region)
126
+ if ec2.describe_route_tables(RouteTableIds=[resource_id])[
127
+ "RouteTables"
128
+ ]:
129
+ return f"{base}/vpcconsole/home?region={region}#RouteTableDetails:RouteTableId={resource_id}"
130
+ case "vol":
131
+ ec2 = boto3.client("ec2", region_name=region)
132
+ if ec2.describe_volumes(VolumeIds=[resource_id])["Volumes"]:
133
+ return f"{base}/ec2/home?region={region}#VolumeDetails:volumeId={resource_id}"
134
+ case "snap":
135
+ ec2 = boto3.client("ec2", region_name=region)
136
+ if ec2.describe_snapshots(SnapshotIds=[resource_id])["Snapshots"]:
137
+ return f"{base}/ec2/home?region={region}#SnapshotDetails:snapshotId={resource_id}"
138
+ case "eni":
139
+ ec2 = boto3.client("ec2", region_name=region)
140
+ if ec2.describe_network_interfaces(NetworkInterfaceIds=[resource_id])["NetworkInterfaces"]:
141
+ return f"{base}/ec2/home?region={region}#NetworkInterface:networkInterfaceId={resource_id}"
142
+ case "nat":
143
+ ec2 = boto3.client("ec2", region_name=region)
144
+ if ec2.describe_nat_gateways(NatGatewayIds=[resource_id])["NatGateways"]:
145
+ return f"{base}/vpcconsole/home?region={region}#NatGatewayDetails:natGatewayId={resource_id}"
146
+ # case "fl":
147
+ # ec2 = boto3.client("ec2", region_name=region)
148
+ # if ec2.describe_flow_logs(FlowLogIds=[resource_id])["FlowLogs"]:
149
+ # return f"{base}/vpcconsole/home?region={region}#FlowLogs:flowLogId={resource_id}"
150
+ case "kms":
151
+ boto3.client("kms", region_name=region).describe_key(
152
+ KeyId=resource_id
153
+ )
154
+ return f"{base}/kms/home?region={region}#/kms/keys/{resource_id}"
155
+ # TODO test this
156
+ case "rds":
157
+ name = resource_id[3:]
158
+ if boto3.client("rds", region_name=region).describe_db_instances(
159
+ DBInstanceIdentifier=name
160
+ )["DBInstances"]:
161
+ return f"{base}/rds/home?region={region}#database:id={name};is-cluster=false"
162
+ except ClientError:
163
+ # not present (or not accessible) in this region — try the next one
164
+ return None
165
+ return None
166
+
167
+ def _not_found(self, resource_id: str) -> NoReturn:
168
+ regions = ", ".join(self.region_cache.get_regions())
169
+ print(
170
+ f"Error: '{resource_id}' not found or not accessible in regions: {regions}",
171
+ file=sys.stderr,
172
+ )
173
+ sys.exit(1)
174
+
@@ -0,0 +1,96 @@
1
+ from pathlib import Path
2
+ import os
3
+ import json
4
+ import time
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ class SessionCache:
9
+ """Persists AWS console federation session hashes to avoid redundant federated sign-ins.
10
+
11
+ The cache is stored as a JSON file at XDG_STATE_HOME/console-sessions.json
12
+ (defaulting to ~/.local/state/console-sessions.json). Each entry maps an AWS
13
+ profile name to its federated_at timestamp and session hash.
14
+ """
15
+
16
+ # Resolve the cache file path from XDG_STATE_HOME or the default ~/.local/state
17
+ PATH = (
18
+ Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
19
+ / "console-sessions.json"
20
+ )
21
+
22
+ def _load_cache(self) -> dict:
23
+ """Read the cache file and return its contents as a dict.
24
+
25
+ Returns an empty dict if the file does not exist or is unreadable/corrupt.
26
+ """
27
+ if self.PATH.exists():
28
+ try:
29
+ return json.loads(self.PATH.read_text())
30
+ except (json.JSONDecodeError, OSError):
31
+ return {}
32
+ return {}
33
+
34
+ def _save_cache(self, cache: dict) -> None:
35
+ """Persist the cache dict to disk, creating parent directories as needed."""
36
+ self.PATH.parent.mkdir(parents=True, exist_ok=True)
37
+ self.PATH.write_text(json.dumps(cache))
38
+
39
+ def get_session_hash(self, profile: str, creds: dict) -> None | str:
40
+ """Return the cached session hash for a profile if it is still valid.
41
+
42
+ A cached entry is considered invalid (returns None) when:
43
+ - No entry exists for the profile.
44
+ - The STS credential Expiration timestamp has already passed.
45
+ - The entry was federated more than 11 hours ago (AWS console session limit).
46
+
47
+ Args:
48
+ profile: The AWS profile name used to look up the cache entry.
49
+ creds: STS credentials dict; may contain an "Expiration" ISO-8601 string.
50
+
51
+ Returns:
52
+ The cached session hash string, or None if the cache is missing/expired.
53
+ """
54
+ cache = self._load_cache()
55
+ entry = cache.get(profile)
56
+
57
+ if not entry:
58
+ return None
59
+
60
+ # Honour the STS credential expiration if present
61
+ expiration = creds.get("Expiration")
62
+ if expiration:
63
+ try:
64
+ exp_time = datetime.fromisoformat(expiration.replace("Z", "+00:00"))
65
+ if datetime.now(timezone.utc) >= exp_time:
66
+ return None
67
+ except ValueError:
68
+ return None
69
+
70
+ # AWS federated console sessions expire after 12 hours; evict after 11 to be safe
71
+ if time.time() - entry["federated_at"] > 11 * 3600:
72
+ return None
73
+
74
+ return entry.get("hash")
75
+
76
+ def record_valid_hash(self, profile: str, hash: str) -> None:
77
+ """Store a newly obtained session hash and evict entries older than 12 hours.
78
+
79
+ Args:
80
+ profile: The AWS profile name to associate with the hash.
81
+ hash: The session hash returned after a successful federation sign-in.
82
+ """
83
+ cache = self._load_cache()
84
+ # Record the new entry with the current epoch time
85
+ cache[profile] = {"federated_at": time.time(), "hash": hash}
86
+ # Prune stale entries to keep the cache file small
87
+ cache = {
88
+ k: v
89
+ for k, v in cache.items()
90
+ if time.time() - v.get("federated_at", 0) < 12 * 3600
91
+ }
92
+ self._save_cache(cache)
93
+
94
+ def reset(self) -> None:
95
+ """Clear the entire session cache."""
96
+ self._save_cache({})
nate2_scripts/test.py ADDED
@@ -0,0 +1,98 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ import urllib.parse
8
+ import webbrowser
9
+ import argparse
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ import argcomplete
14
+ import boto3
15
+ from botocore.exceptions import ClientError
16
+
17
+ def get_credentials() -> dict:
18
+ result = subprocess.run(
19
+ ["aws", "configure", "export-credentials"],
20
+ capture_output=True,
21
+ text=True,
22
+ )
23
+ if result.returncode != 0:
24
+ print(f"Error: Failed to export credentials.\n{result.stderr}", file=sys.stderr)
25
+ sys.exit(1)
26
+ return json.loads(result.stdout)
27
+
28
+
29
+ def get_signin_token(creds: dict) -> str:
30
+ session = json.dumps(
31
+ {
32
+ "sessionId": creds["AccessKeyId"],
33
+ "sessionKey": creds["SecretAccessKey"],
34
+ "sessionToken": creds["SessionToken"],
35
+ }
36
+ )
37
+ sess = urllib.parse.quote(session)
38
+ token_url = (
39
+ f"https://signin.aws.amazon.com/federation?Action=getSigninToken&Session={sess}"
40
+ )
41
+ result = subprocess.run(
42
+ ["curl", "-s", token_url], capture_output=True, text=True, check=True
43
+ )
44
+ try:
45
+ return json.loads(result.stdout)["SigninToken"]
46
+ except json.JSONDecodeError:
47
+ print("Error: Failed to parse JSON response. You may not have credentials.")
48
+ sys.exit(1)
49
+
50
+ creds = get_credentials()
51
+ print(creds)
52
+
53
+ signin_token = get_signin_token(creds)
54
+
55
+ print(signin_token)
56
+
57
+ # Build the federation login URL
58
+ destination = "https://us-east-1.console.aws.amazon.com/console/home?region=us-east-1"
59
+ dest_encoded = urllib.parse.quote(destination, safe="")
60
+ login_url = f"https://signin.aws.amazon.com/federation?Action=login&Issuer=&Destination={dest_encoded}&SigninToken={signin_token}"
61
+
62
+ # Follow redirects and capture the final URL
63
+ # print("\nFollowing federation redirect chain...")
64
+ # result = subprocess.run(
65
+ # ["curl", "-s", "-L", "-o", "/dev/null", "-w", "%{url_effective}", login_url],
66
+ # capture_output=True,
67
+ # text=True,
68
+ # )
69
+ # final_url = result.stdout.strip()
70
+ # print(f"Final URL: {final_url}")
71
+
72
+ print(" ".join(["curl", "-s", "-L", "-v", login_url]))
73
+ sys.exit(0)
74
+ # Also check just the first redirect (without following)
75
+ signin_token2 = get_signin_token(creds)
76
+ login_url2 = f"https://signin.aws.amazon.com/federation?Action=login&Issuer=&Destination={dest_encoded}&SigninToken={signin_token2}"
77
+
78
+ result2 = subprocess.run(
79
+ ["curl", "-s", "-o", "/dev/null", "-w", "%{redirect_url}", login_url2],
80
+ capture_output=True,
81
+ text=True,
82
+ )
83
+ first_redirect = result2.stdout.strip()
84
+ print(f"First redirect URL: {first_redirect}")
85
+
86
+ # Check if either contains the account-specific hash pattern
87
+ pattern = r"(\d{12})-([a-z0-9]+)\.[a-z0-9-]+\.console\.aws\.amazon\.com"
88
+ for label, url in [("Final", final_url), ("First redirect", first_redirect)]:
89
+ match = re.search(pattern, url)
90
+ if match:
91
+ account_id, console_hash = match.groups()
92
+ print(f"\n>>> Found hash in {label} URL!")
93
+ print(f" Account ID: {account_id}")
94
+ print(f" Console hash: {console_hash}")
95
+ break
96
+ else:
97
+ print("\n>>> No account-specific hash found in redirects.")
98
+ print(" The rewrite is likely done client-side by JavaScript.")
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: nate2-scripts
3
+ Version: 1.0.0
4
+ Summary: Utility scripts for AWS development on macOS
5
+ Project-URL: Homepage, https://github.com/tennex-nwestfall/nate2-scripts
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: argcomplete
9
+ Requires-Dist: boto3
@@ -0,0 +1,15 @@
1
+ nate2_scripts/__init__.py,sha256=Odk0uF1h2RuL1xKI0M2Fp8A_Be7pT0PLkAw2nB9wC9M,101
2
+ nate2_scripts/bing_wrapper.py,sha256=AKeIYAfFCFDVkP3PMdj7TEx25M_JpYRJHMN9SRqln5U,4694
3
+ nate2_scripts/test.py,sha256=IlsOWc3eE_komf774xFBisu2ui1a28QmVPcySsVeG9s,3204
4
+ nate2_scripts/console/__init__.py,sha256=_PGvw96WrEQNjJOZe0RmRvOClZ44L_y8hXY8P88AG_o,5050
5
+ nate2_scripts/console/arnResolver.py,sha256=tiBw_XVlEWtj322jZWk0RVz9Pe6fdBk2C-lzMOPhxUE,24934
6
+ nate2_scripts/console/destitationResolver.py,sha256=myZW81U7Deh0nPv4jfIvNVd8_XFf3d22_VX0_7ExjiY,5630
7
+ nate2_scripts/console/hashGrabber.py,sha256=ydSqDCqCwG1YaqO5WKWWqMm0dLxR9GODX7BwYyXpcFo,6437
8
+ nate2_scripts/console/regionCache.py,sha256=2yHSfP1bXBqL3QoSVEXw1pO4cuCIIYjm7h1qOYAwfWc,1812
9
+ nate2_scripts/console/resourceIdResolver.py,sha256=QH7QA17n-jsJ7yj9ulyHP6MFT94jochXi9CX0-xkcqY,8185
10
+ nate2_scripts/console/sessionCache.py,sha256=q60C_0vIg8h5oPP1Bh3rTdrMPn5mEkPQz7FR8DhrHmA,3562
11
+ nate2_scripts-1.0.0.dist-info/METADATA,sha256=ibx35mXSfjLFA7bdsqxzTgvQlI9jgOwfwzbd4DjllgY,278
12
+ nate2_scripts-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
13
+ nate2_scripts-1.0.0.dist-info/entry_points.txt,sha256=PvmjaUn-B1nck47azFCr4HYs0si1LRe-hMqEYwvIFCM,102
14
+ nate2_scripts-1.0.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
15
+ nate2_scripts-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ bing-wrapper = nate2_scripts.bing_wrapper:main
3
+ console = nate2_scripts.console:main