Skip to content

Commit b1f0cba

Browse files
committed
send /secrets endpoint error to stdout
1 parent eb7d4cb commit b1f0cba

File tree

1 file changed

+74
-64
lines changed

1 file changed

+74
-64
lines changed

EnumSecrets.py

Lines changed: 74 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python3
22
import argparse
3+
import base64
34
import json
45
import sys
5-
import base64
66
from pathlib import Path
77

88
import requests
@@ -32,18 +32,30 @@ def get_engine_info(base_url: str, timeout: int = 10) -> dict:
3232

3333

3434
def get_secrets(base_url: str, timeout: int = 10):
35-
"""Return list of secrets (metadata)."""
35+
"""
36+
Return (secrets_list, error_message).
37+
If the Docker API returns an error (e.g., not a Swarm manager), we parse and return it.
38+
"""
3639
try:
3740
resp = requests.get(f"{base_url.rstrip('/')}/secrets", timeout=timeout)
38-
resp.raise_for_status()
39-
return resp.json()
41+
if resp.status_code != 200:
42+
# Try to extract { "message": "..." } or fall back to text
43+
err = None
44+
try:
45+
j = resp.json()
46+
err = j.get("message") or j
47+
except Exception:
48+
err = (resp.text or "").strip()
49+
if not err:
50+
err = f"HTTP {resp.status_code}"
51+
return [], str(err)
52+
return resp.json(), None
4053
except requests.RequestException as e:
41-
print(f"[!] Failed to get secrets from {base_url}: {e}", file=sys.stderr)
42-
return []
54+
return [], f"Request failed: {e}"
4355

4456

4557
def get_secret_detail(base_url: str, secret_id: str, timeout: int = 10):
46-
"""Return secret detail (usually metadata only; value is not exposed by Docker API)."""
58+
"""Return secret detail (usually metadata only)."""
4759
try:
4860
resp = requests.get(f"{base_url.rstrip('/')}/secrets/{secret_id}", timeout=timeout)
4961
resp.raise_for_status()
@@ -54,32 +66,24 @@ def get_secret_detail(base_url: str, secret_id: str, timeout: int = 10):
5466

5567

5668
def maybe_decode_base64(val):
57-
"""Try to base64-decode a string; return (decoded_text, success_flag)."""
69+
"""Try to base64-decode a string; return (decoded_text_or_bytes, success_flag)."""
5870
if not isinstance(val, (bytes, str)):
5971
return None, False
60-
if isinstance(val, str):
61-
s = val.strip()
62-
# base64 requires length multiple of 4; try padding
63-
pad_len = (-len(s)) % 4
64-
s_padded = s + ("=" * pad_len)
65-
try:
66-
raw = base64.b64decode(s_padded, validate=False)
67-
try:
68-
return raw.decode("utf-8", errors="replace"), True
69-
except Exception:
70-
return raw, True
71-
except Exception:
72-
return None, False
73-
else:
74-
# bytes; try to decode directly
75-
try:
72+
try:
73+
if isinstance(val, str):
74+
s = val.strip()
75+
pad_len = (-len(s)) % 4
76+
s = s + ("=" * pad_len)
77+
raw = base64.b64decode(s, validate=False)
78+
else:
7679
raw = base64.b64decode(val, validate=False)
77-
try:
78-
return raw.decode("utf-8", errors="replace"), True
79-
except Exception:
80-
return raw, True
81-
except Exception:
82-
return None, False
80+
except Exception:
81+
return None, False
82+
83+
try:
84+
return raw.decode("utf-8", errors="replace"), True
85+
except Exception:
86+
return raw, True
8387

8488

8589
def parse_args():
@@ -126,11 +130,28 @@ def main():
126130
if args.show_info_json and engine_info:
127131
print(json.dumps(engine_info, indent=2))
128132

129-
# 2) Secrets enumeration
130-
secrets = get_secrets(args.url, timeout=args.timeout)
133+
# 2) Secrets enumeration (with error capture)
134+
secrets, err = get_secrets(args.url, timeout=args.timeout)
135+
136+
results = {"engine_info": engine_info, "secrets": []}
137+
if err:
138+
# Mirror the Docker API error clearly for the user
139+
print(f"[!] Failed to enumerate secrets: {err}", file=sys.stderr)
140+
# Common hint for Swarm-related errors
141+
if "swarm" in err.lower() and "manager" in err.lower():
142+
print("Hint: Connect to a Swarm **manager** node to enumerate secrets.", file=sys.stderr)
143+
# Save partial output if requested
144+
if args.out:
145+
try:
146+
Path(args.out).write_text(json.dumps({**results, "error": err}, indent=2))
147+
print(f"\nSaved results (with error) to {Path(args.out).resolve()}")
148+
except Exception as e:
149+
print(f"[!] Failed to write output file '{args.out}': {e}", file=sys.stderr)
150+
return 2
151+
return 0 # Exit gracefully; we surfaced the error
152+
131153
if not secrets:
132154
print("No secrets found.")
133-
results = {"engine_info": engine_info, "secrets": []}
134155
if args.out:
135156
try:
136157
Path(args.out).write_text(json.dumps(results, indent=2))
@@ -141,63 +162,52 @@ def main():
141162
return 0
142163

143164
print(f"Found {len(secrets)} secrets. Inspecting")
144-
results = {"engine_info": engine_info, "secrets": []}
145165

166+
# 3) Inspect each secret (metadata; value usually unavailable)
167+
from alive_progress import alive_bar # local import to speed startup if not used
146168
with alive_bar(len(secrets), title="Investigating secrets") as bar:
147169
for s in secrets:
148170
secret_id = s.get("ID") or s.get("Id") or ""
149171
spec = s.get("Spec", {}) or {}
150172
name = spec.get("Name") or s.get("Name") or "(unnamed)"
151173

152-
# Fetch detail (usually metadata only)
153174
detail = get_secret_detail(args.url, secret_id, timeout=args.timeout) or {}
154175

155-
# Attempt to find a value-like field (non-standard)
156-
decoded_text = None
157-
value_found = False
158-
if args.attempt-values: # <-- hyphen not allowed in var name; fix below
159-
pass
160-
# ^ We'll fix in code block below.
161-
162-
# Print
163176
print(f"\nSecret Name: {name}")
164177
print(f"Secret ID: {secret_id}")
178+
165179
if args.attempt_values:
166-
# Look in a few plausible spots
167180
raw_val = (
168181
detail.get("Spec", {}).get("Data")
169182
or detail.get("Spec", {}).get("Value")
170183
or detail.get("Data")
171184
or detail.get("Value")
172185
)
173186
if raw_val is not None:
174-
decoded_text, value_found = maybe_decode_base64(raw_val)
175-
if value_found:
176-
# If bytes, show repr; if text, show text
177-
if isinstance(decoded_text, (bytes, bytearray)):
178-
print("Secret Value (decoded bytes):", decoded_text)
187+
decoded, ok = maybe_decode_base64(raw_val)
188+
if ok:
189+
if isinstance(decoded, (bytes, bytearray)):
190+
print("Secret Value (decoded bytes):", decoded)
191+
else:
192+
print("Secret Value (decoded):")
193+
for line in str(decoded).splitlines() or ["(empty)"]:
194+
print(f" {line}")
195+
results["secrets"].append(
196+
{"id": secret_id, "name": name, "detail": detail, "decoded_value": decoded}
197+
)
179198
else:
180-
print("Secret Value (decoded):")
181-
# indent multi-line for readability
182-
for line in str(decoded_text).splitlines() or ["(empty)"]:
183-
print(f" {line}")
199+
print("Secret Value: (present but could not decode)")
200+
results["secrets"].append({"id": secret_id, "name": name, "detail": detail})
184201
else:
185-
print("Secret Value: (not available via Docker API or could not decode)")
202+
print("Secret Value: (not available via Docker API)")
203+
results["secrets"].append({"id": secret_id, "name": name, "detail": detail})
186204
else:
187205
print("Secret Value: (skipped; use --attempt-values to try extracting when available)")
188-
189-
results["secrets"].append(
190-
{
191-
"id": secret_id,
192-
"name": name,
193-
"detail": detail,
194-
**({"decoded_value": decoded_text} if value_found else {}),
195-
}
196-
)
206+
results["secrets"].append({"id": secret_id, "name": name, "detail": detail})
197207

198208
bar() # progress
199209

200-
# 3) Save if requested
210+
# 4) Save if requested
201211
if args.out:
202212
try:
203213
out_path = Path(args.out)

0 commit comments

Comments
 (0)