gitarsenal-cli 1.2.4 โ 1.2.6
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.
package/package.json
CHANGED
package/python/README.md
CHANGED
@@ -18,13 +18,15 @@ The GitArsenal CLI integrates with Modal for creating sandboxes and SSH containe
|
|
18
18
|
For enhanced security, GitArsenal CLI now automatically cleans up Modal tokens after SSH containers are created. This ensures that your Modal token is not left in the environment or on disk after the container is started.
|
19
19
|
|
20
20
|
The cleanup process:
|
21
|
-
1. Removes Modal token environment variables (MODAL_TOKEN_ID, MODAL_TOKEN, MODAL_TOKEN_SECRET)
|
22
|
-
2. Deletes Modal token files (.modal/token.json, .modal/token_alt.json, .modalconfig)
|
21
|
+
1. Removes real Modal token environment variables (MODAL_TOKEN_ID, MODAL_TOKEN, MODAL_TOKEN_SECRET)
|
22
|
+
2. Deletes real Modal token files (.modal/token.json, .modal/token_alt.json, .modalconfig)
|
23
23
|
3. Invalidates Modal sessions by removing session files and directories
|
24
|
-
4.
|
25
|
-
5.
|
24
|
+
4. Creates corrupted token files that will cause Modal CLI authentication to fail
|
25
|
+
5. Creates a wrapper script around the Modal CLI that specifically blocks the `modal shell` command
|
26
|
+
6. Runs automatically after SSH container creation
|
27
|
+
7. Runs on service shutdown via signal handlers
|
26
28
|
|
27
|
-
This prevents potential token leakage when containers are shared or when the service is stopped, and ensures that users cannot continue to use Modal CLI commands after the container is created.
|
29
|
+
This prevents potential token leakage when containers are shared or when the service is stopped, and ensures that users cannot continue to use Modal CLI commands like `modal shell` after the container is created.
|
28
30
|
|
29
31
|
## Usage
|
30
32
|
|
Binary file
|
@@ -215,7 +215,7 @@ def cleanup_modal_token():
|
|
215
215
|
logger.info("๐งน Cleaning up Modal token for security...")
|
216
216
|
|
217
217
|
try:
|
218
|
-
# Remove token from environment variables
|
218
|
+
# Remove real token from environment variables
|
219
219
|
if "MODAL_TOKEN_ID" in os.environ:
|
220
220
|
del os.environ["MODAL_TOKEN_ID"]
|
221
221
|
logger.info("โ
Removed MODAL_TOKEN_ID from environment")
|
@@ -276,6 +276,79 @@ def cleanup_modal_token():
|
|
276
276
|
except Exception as e:
|
277
277
|
logger.warning(f"โ ๏ธ Error during Modal session invalidation: {e}")
|
278
278
|
|
279
|
+
# Create corrupted token files that will cause Modal CLI to fail
|
280
|
+
try:
|
281
|
+
logger.info("๐ Creating corrupted Modal token files...")
|
282
|
+
|
283
|
+
# Create .modal directory if it doesn't exist
|
284
|
+
if not modal_dir.exists():
|
285
|
+
modal_dir.mkdir(parents=True)
|
286
|
+
logger.info(f"โ
Created Modal directory at {modal_dir}")
|
287
|
+
|
288
|
+
# Create a corrupted token.json file
|
289
|
+
token_file = modal_dir / "token.json"
|
290
|
+
with open(token_file, 'w') as f:
|
291
|
+
f.write('{"corrupted": true, "message": "Token has been invalidated for security reasons"}')
|
292
|
+
logger.info(f"โ
Created corrupted token file at {token_file}")
|
293
|
+
|
294
|
+
# Create a corrupted .modalconfig file
|
295
|
+
modalconfig_file = Path.home() / ".modalconfig"
|
296
|
+
with open(modalconfig_file, 'w') as f:
|
297
|
+
f.write("# This file has been corrupted for security reasons\n")
|
298
|
+
f.write("corrupted = true\n")
|
299
|
+
logger.info(f"โ
Created corrupted .modalconfig file at {modalconfig_file}")
|
300
|
+
|
301
|
+
# Try to disable the modal shell command specifically
|
302
|
+
try:
|
303
|
+
# Find the modal executable path
|
304
|
+
result = subprocess.run(
|
305
|
+
["which", "modal"],
|
306
|
+
capture_output=True,
|
307
|
+
text=True
|
308
|
+
)
|
309
|
+
modal_path = result.stdout.strip()
|
310
|
+
|
311
|
+
if modal_path:
|
312
|
+
logger.info(f"๐ Found Modal CLI at: {modal_path}")
|
313
|
+
|
314
|
+
# Create a wrapper script that intercepts the shell command
|
315
|
+
modal_shell_wrapper = """#!/bin/bash
|
316
|
+
if [[ "$1" == "shell" ]]; then
|
317
|
+
echo "โ Modal shell has been disabled for security reasons."
|
318
|
+
echo "Modal token has been removed after SSH container creation."
|
319
|
+
exit 1
|
320
|
+
fi
|
321
|
+
|
322
|
+
# Pass all other commands to the real modal CLI
|
323
|
+
REAL_MODAL=$(which modal.real 2>/dev/null)
|
324
|
+
if [ -n "$REAL_MODAL" ]; then
|
325
|
+
exec "$REAL_MODAL" "$@"
|
326
|
+
else
|
327
|
+
echo "โ Modal CLI has been disabled for security reasons."
|
328
|
+
echo "Modal token has been removed after SSH container creation."
|
329
|
+
exit 1
|
330
|
+
fi
|
331
|
+
"""
|
332
|
+
|
333
|
+
# Rename the original modal executable
|
334
|
+
os.rename(modal_path, f"{modal_path}.real")
|
335
|
+
logger.info(f"โ
Renamed original Modal CLI to {modal_path}.real")
|
336
|
+
|
337
|
+
# Create a new modal script
|
338
|
+
with open(modal_path, 'w') as f:
|
339
|
+
f.write(modal_shell_wrapper)
|
340
|
+
|
341
|
+
# Make it executable
|
342
|
+
os.chmod(modal_path, 0o755)
|
343
|
+
logger.info(f"โ
Created Modal CLI wrapper at {modal_path}")
|
344
|
+
else:
|
345
|
+
logger.warning("โ ๏ธ Could not find Modal CLI path")
|
346
|
+
except Exception as e:
|
347
|
+
logger.warning(f"โ ๏ธ Error creating Modal CLI wrapper: {e}")
|
348
|
+
|
349
|
+
except Exception as e:
|
350
|
+
logger.warning(f"โ ๏ธ Error creating corrupted token files: {e}")
|
351
|
+
|
279
352
|
logger.info("โ
Modal token cleanup completed successfully")
|
280
353
|
except Exception as e:
|
281
354
|
logger.error(f"โ Error during Modal token cleanup: {e}")
|
@@ -3298,7 +3298,7 @@ def cleanup_modal_token():
|
|
3298
3298
|
print("๐งน Cleaning up Modal token for security...")
|
3299
3299
|
|
3300
3300
|
try:
|
3301
|
-
# Remove token from environment variables
|
3301
|
+
# Remove real token from environment variables
|
3302
3302
|
if "MODAL_TOKEN_ID" in os.environ:
|
3303
3303
|
del os.environ["MODAL_TOKEN_ID"]
|
3304
3304
|
print("โ
Removed MODAL_TOKEN_ID from environment")
|
@@ -3332,12 +3332,12 @@ def cleanup_modal_token():
|
|
3332
3332
|
if os.path.exists(modalconfig_file):
|
3333
3333
|
os.remove(modalconfig_file)
|
3334
3334
|
print(f"โ
Deleted .modalconfig file at {modalconfig_file}")
|
3335
|
-
|
3335
|
+
|
3336
3336
|
# Try to invalidate Modal sessions
|
3337
3337
|
try:
|
3338
3338
|
print("๐ Invalidating Modal sessions...")
|
3339
3339
|
|
3340
|
-
#
|
3340
|
+
# Try to directly modify Modal's session files
|
3341
3341
|
try:
|
3342
3342
|
# Check for session files in .modal directory
|
3343
3343
|
session_dir = os.path.join(modal_dir, "sessions")
|
@@ -3359,6 +3359,82 @@ def cleanup_modal_token():
|
|
3359
3359
|
except Exception as e:
|
3360
3360
|
print(f"โ ๏ธ Error during Modal session invalidation: {e}")
|
3361
3361
|
|
3362
|
+
# Create corrupted token files that will cause Modal CLI to fail
|
3363
|
+
try:
|
3364
|
+
print("๐ Creating corrupted Modal token files...")
|
3365
|
+
|
3366
|
+
# Create .modal directory if it doesn't exist
|
3367
|
+
if not os.path.exists(modal_dir):
|
3368
|
+
os.makedirs(modal_dir)
|
3369
|
+
print(f"โ
Created Modal directory at {modal_dir}")
|
3370
|
+
|
3371
|
+
# Create a corrupted token.json file
|
3372
|
+
token_file = os.path.join(modal_dir, "token.json")
|
3373
|
+
with open(token_file, 'w') as f:
|
3374
|
+
f.write('{"corrupted": true, "message": "Token has been invalidated for security reasons"}')
|
3375
|
+
print(f"โ
Created corrupted token file at {token_file}")
|
3376
|
+
|
3377
|
+
# Create a corrupted .modalconfig file
|
3378
|
+
modalconfig_file = os.path.join(home_dir, ".modalconfig")
|
3379
|
+
with open(modalconfig_file, 'w') as f:
|
3380
|
+
f.write("# This file has been corrupted for security reasons\n")
|
3381
|
+
f.write("corrupted = true\n")
|
3382
|
+
print(f"โ
Created corrupted .modalconfig file at {modalconfig_file}")
|
3383
|
+
|
3384
|
+
# Try to disable the modal shell command specifically
|
3385
|
+
try:
|
3386
|
+
import subprocess
|
3387
|
+
import sys
|
3388
|
+
|
3389
|
+
# Find the modal executable path
|
3390
|
+
result = subprocess.run(
|
3391
|
+
["which", "modal"],
|
3392
|
+
capture_output=True,
|
3393
|
+
text=True
|
3394
|
+
)
|
3395
|
+
modal_path = result.stdout.strip()
|
3396
|
+
|
3397
|
+
if modal_path:
|
3398
|
+
print(f"๐ Found Modal CLI at: {modal_path}")
|
3399
|
+
|
3400
|
+
# Create a wrapper script that intercepts the shell command
|
3401
|
+
modal_shell_wrapper = """#!/bin/bash
|
3402
|
+
if [[ "$1" == "shell" ]]; then
|
3403
|
+
echo "โ Modal shell has been disabled for security reasons."
|
3404
|
+
echo "Modal token has been removed after SSH container creation."
|
3405
|
+
exit 1
|
3406
|
+
fi
|
3407
|
+
|
3408
|
+
# Pass all other commands to the real modal CLI
|
3409
|
+
REAL_MODAL=$(which modal.real 2>/dev/null)
|
3410
|
+
if [ -n "$REAL_MODAL" ]; then
|
3411
|
+
exec "$REAL_MODAL" "$@"
|
3412
|
+
else
|
3413
|
+
echo "โ Modal CLI has been disabled for security reasons."
|
3414
|
+
echo "Modal token has been removed after SSH container creation."
|
3415
|
+
exit 1
|
3416
|
+
fi
|
3417
|
+
"""
|
3418
|
+
|
3419
|
+
# Rename the original modal executable
|
3420
|
+
os.rename(modal_path, f"{modal_path}.real")
|
3421
|
+
print(f"โ
Renamed original Modal CLI to {modal_path}.real")
|
3422
|
+
|
3423
|
+
# Create a new modal script
|
3424
|
+
with open(modal_path, 'w') as f:
|
3425
|
+
f.write(modal_shell_wrapper)
|
3426
|
+
|
3427
|
+
# Make it executable
|
3428
|
+
os.chmod(modal_path, 0o755)
|
3429
|
+
print(f"โ
Created Modal CLI wrapper at {modal_path}")
|
3430
|
+
else:
|
3431
|
+
print("โ ๏ธ Could not find Modal CLI path")
|
3432
|
+
except Exception as e:
|
3433
|
+
print(f"โ ๏ธ Error creating Modal CLI wrapper: {e}")
|
3434
|
+
|
3435
|
+
except Exception as e:
|
3436
|
+
print(f"โ ๏ธ Error creating corrupted token files: {e}")
|
3437
|
+
|
3362
3438
|
print("โ
Modal token cleanup completed successfully")
|
3363
3439
|
except Exception as e:
|
3364
3440
|
print(f"โ Error during Modal token cleanup: {e}")
|
@@ -108,38 +108,76 @@ def verify_token_cleaned_up():
|
|
108
108
|
"""Verify that the test token has been cleaned up"""
|
109
109
|
print("๐ Verifying token cleanup...")
|
110
110
|
|
111
|
-
# Check environment variables
|
111
|
+
# Check that the original token is not in environment variables
|
112
112
|
env_token_id = os.environ.get("MODAL_TOKEN_ID")
|
113
113
|
env_token = os.environ.get("MODAL_TOKEN")
|
114
114
|
env_token_secret = os.environ.get("MODAL_TOKEN_SECRET")
|
115
115
|
|
116
|
-
|
117
|
-
|
116
|
+
# Check that original tokens are removed or replaced with invalid tokens
|
117
|
+
if env_token_id and not ("invalid" in env_token_id or "placeholder" in env_token_id):
|
118
|
+
print(f"โ Original MODAL_TOKEN_ID still exists: '{env_token_id}'")
|
118
119
|
return False
|
119
120
|
|
120
|
-
if env_token:
|
121
|
-
print(f"โ MODAL_TOKEN still exists: '{env_token}'")
|
121
|
+
if env_token and not ("invalid" in env_token or "placeholder" in env_token):
|
122
|
+
print(f"โ Original MODAL_TOKEN still exists: '{env_token}'")
|
122
123
|
return False
|
123
124
|
|
124
|
-
if env_token_secret:
|
125
|
-
print(f"โ MODAL_TOKEN_SECRET still exists: '{env_token_secret}'")
|
125
|
+
if env_token_secret and not ("invalid" in env_token_secret or "placeholder" in env_token_secret):
|
126
|
+
print(f"โ Original MODAL_TOKEN_SECRET still exists: '{env_token_secret}'")
|
126
127
|
return False
|
127
128
|
|
128
129
|
# Check token files
|
129
130
|
modal_dir = Path.home() / ".modal"
|
130
131
|
token_file = modal_dir / "token.json"
|
131
|
-
if token_file.exists():
|
132
|
-
print(f"โ Token file still exists at {token_file}")
|
133
|
-
return False
|
134
132
|
|
135
|
-
|
136
|
-
if
|
137
|
-
|
133
|
+
# Check if token.json contains invalid values
|
134
|
+
if token_file.exists():
|
135
|
+
try:
|
136
|
+
import json
|
137
|
+
with open(token_file, 'r') as f:
|
138
|
+
token_data = json.load(f)
|
139
|
+
|
140
|
+
if "token_id" in token_data:
|
141
|
+
if not ("invalid" in token_data["token_id"] or "placeholder" in token_data["token_id"]):
|
142
|
+
print(f"โ Token file contains original token_id: {token_data['token_id']}")
|
143
|
+
return False
|
144
|
+
print(f"โ
Token file contains invalid token_id: {token_data['token_id']}")
|
145
|
+
else:
|
146
|
+
print("โ Token file does not contain token_id")
|
147
|
+
return False
|
148
|
+
|
149
|
+
if "token_secret" in token_data:
|
150
|
+
if not ("invalid" in token_data["token_secret"] or "placeholder" in token_data["token_secret"]):
|
151
|
+
print(f"โ Token file contains original token_secret")
|
152
|
+
return False
|
153
|
+
print(f"โ
Token file contains invalid token_secret")
|
154
|
+
else:
|
155
|
+
print("โ Token file does not contain token_secret")
|
156
|
+
return False
|
157
|
+
except Exception as e:
|
158
|
+
print(f"โ Error reading token file: {e}")
|
159
|
+
return False
|
160
|
+
else:
|
161
|
+
print("โ Token file does not exist")
|
138
162
|
return False
|
139
163
|
|
164
|
+
# Check .modalconfig file
|
140
165
|
modalconfig_file = Path.home() / ".modalconfig"
|
141
166
|
if modalconfig_file.exists():
|
142
|
-
|
167
|
+
try:
|
168
|
+
with open(modalconfig_file, 'r') as f:
|
169
|
+
config_content = f.read()
|
170
|
+
|
171
|
+
if not ("invalid" in config_content or "placeholder" in config_content):
|
172
|
+
print("โ .modalconfig file does not contain invalid tokens")
|
173
|
+
return False
|
174
|
+
|
175
|
+
print("โ
.modalconfig file contains invalid tokens")
|
176
|
+
except Exception as e:
|
177
|
+
print(f"โ Error reading .modalconfig file: {e}")
|
178
|
+
return False
|
179
|
+
else:
|
180
|
+
print("โ .modalconfig file does not exist")
|
143
181
|
return False
|
144
182
|
|
145
183
|
# Check if Modal sessions directory exists
|
@@ -148,27 +186,43 @@ def verify_token_cleaned_up():
|
|
148
186
|
print(f"โ Modal sessions directory still exists at {session_dir}")
|
149
187
|
return False
|
150
188
|
|
151
|
-
# Check for any
|
189
|
+
# Check for any session or auth files
|
152
190
|
if modal_dir.exists():
|
153
191
|
for file in os.listdir(modal_dir):
|
154
192
|
if "session" in file.lower() or "auth" in file.lower():
|
155
193
|
print(f"โ Modal session/auth file still exists: {modal_dir / file}")
|
156
194
|
return False
|
157
195
|
|
158
|
-
# Check if Modal CLI
|
196
|
+
# Check if Modal CLI works with invalid tokens
|
159
197
|
try:
|
160
198
|
import subprocess
|
161
199
|
result = subprocess.run(
|
162
|
-
["modal", "
|
200
|
+
["modal", "--help"],
|
201
|
+
capture_output=True,
|
202
|
+
text=True
|
203
|
+
)
|
204
|
+
if result.returncode == 0:
|
205
|
+
print("โ
Modal CLI works with invalid tokens")
|
206
|
+
else:
|
207
|
+
print("โ Modal CLI does not work with invalid tokens")
|
208
|
+
print(f"Output: {result.stdout}")
|
209
|
+
print(f"Error: {result.stderr}")
|
210
|
+
return False
|
211
|
+
|
212
|
+
# Check that operations requiring authentication fail
|
213
|
+
result = subprocess.run(
|
214
|
+
["modal", "app", "list"],
|
163
215
|
capture_output=True,
|
164
216
|
text=True
|
165
217
|
)
|
166
|
-
if result.returncode
|
167
|
-
print("
|
218
|
+
if result.returncode != 0 or "unauthorized" in result.stderr.lower() or "error" in result.stderr.lower():
|
219
|
+
print("โ
Modal operations requiring authentication fail as expected")
|
220
|
+
else:
|
221
|
+
print("โ Modal operations requiring authentication still work")
|
168
222
|
print(f"Output: {result.stdout}")
|
169
223
|
return False
|
170
224
|
except Exception as e:
|
171
|
-
print(f"โ ๏ธ Error checking Modal CLI
|
225
|
+
print(f"โ ๏ธ Error checking Modal CLI status: {e}")
|
172
226
|
|
173
227
|
print("โ
Token has been cleaned up successfully")
|
174
228
|
return True
|