Compare commits
2 Commits
runner-gua
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f19e23eaf | ||
| ef96051d6f |
64
api/tests/test_lockbox_names.py
Normal file
64
api/tests/test_lockbox_names.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""lockbox-names: headings + labels + resolvable, NEVER a value or prose after a label."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
V1 = "Qm7xT2vLp9RkZw4HnB8dYc3S" # synthetic values
|
||||||
|
V2 = "other-fake-value-1234567890"
|
||||||
|
V3 = "dup-one-aaaaaaaaaaaaaaaa"
|
||||||
|
V4 = "dup-two-bbbbbbbbbbbbbbbb"
|
||||||
|
PROSE = "ZZPROSEZZ-not-for-printing"
|
||||||
|
|
||||||
|
|
||||||
|
def make(tmp_path):
|
||||||
|
r = tmp_path / "kit"
|
||||||
|
(r / "secrets" / "x").mkdir(parents=True)
|
||||||
|
(r / "ACCESS_LOCKBOX.md").write_text(
|
||||||
|
"# LOCKBOX\n\n## 🔷 AZURE signing (added 10-01)\n"
|
||||||
|
f"- **Tenant:** {PROSE} lives in the portal\n"
|
||||||
|
f"- **`AZURE_CLIENT_ID`**: `{V1}`\n"
|
||||||
|
f"- **Secret (AZURE_CLIENT_SECRET):** `{V2}`\n"
|
||||||
|
"## GOOGLE oauth\n"
|
||||||
|
f"GOOGLE_OAUTH_CLIENT_ID={V2}\n"
|
||||||
|
f"- **`DUP_KEY`**: `{V3}`\n- **`DUP_KEY`**: `{V4}`\n"
|
||||||
|
f"## stray\n**{V1}** is a heading-like bold that is secret shaped? no, just label\n")
|
||||||
|
(r / "secrets" / "x" / "a.env").write_text(f"FILE_KEY={V1}\n")
|
||||||
|
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=r, check=True)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def run(r, *args):
|
||||||
|
e = {**os.environ, "LOCKBOX_REPO": str(r), "LOCKBOX_REF": "WORKTREE"}
|
||||||
|
p = subprocess.run([sys.executable, str(ROOT / "scripts" / "lockbox_names.py"), *args],
|
||||||
|
capture_output=True, text=True, env=e)
|
||||||
|
return p.returncode, p.stdout + p.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_lists_labels_and_resolvable_without_values_or_prose(tmp_path):
|
||||||
|
r = make(tmp_path)
|
||||||
|
rc, out = run(r, "AZURE|GOOGLE|FILE|DUP")
|
||||||
|
assert rc == 0
|
||||||
|
assert "AZURE signing (added 10-01) | AZURE_CLIENT_ID | md | yes" in out
|
||||||
|
assert "| GOOGLE_OAUTH_CLIENT_ID | env | yes" in out
|
||||||
|
assert "| FILE_KEY | file | yes" in out
|
||||||
|
assert "| DUP_KEY | md | dup" in out
|
||||||
|
# a prose label is listed but never resolvable, and nothing after the label leaks
|
||||||
|
assert "| Tenant: " not in out or "| Tenant" in out
|
||||||
|
assert "| label | no" in out
|
||||||
|
for secret in (V1, V2, V3, V4, PROSE, "lives in the portal"):
|
||||||
|
assert secret not in out
|
||||||
|
for i in range(0, len(secret) - 7):
|
||||||
|
assert secret[i:i + 8] not in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_and_bad_regex(tmp_path):
|
||||||
|
r = make(tmp_path)
|
||||||
|
rc, out = run(r, "NOSUCHTHING")
|
||||||
|
assert rc == 0 and "0 entries" in out
|
||||||
|
rc, out = run(r, "(")
|
||||||
|
assert rc == 2 and "bad regex" in out
|
||||||
@@ -5,10 +5,10 @@ set -euo pipefail
|
|||||||
here=$(cd "$(dirname "$0")" && pwd)
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
dest="$HOME/.local/share/secret-tools"
|
dest="$HOME/.local/share/secret-tools"
|
||||||
mkdir -p "$dest" "$HOME/.local/bin"
|
mkdir -p "$dest" "$HOME/.local/bin"
|
||||||
cp "$here/secret_shapes.py" "$here/secret_scan.py" "$here/env_names.py" "$here/lockbox_put.py" "$dest/"
|
cp "$here/secret_shapes.py" "$here/secret_scan.py" "$here/env_names.py" "$here/lockbox_put.py" "$here/lockbox_names.py" "$dest/"
|
||||||
for pair in "secret-scan:secret_scan.py" "env-names:env_names.py" "lockbox-put:lockbox_put.py"; do
|
for pair in "secret-scan:secret_scan.py" "env-names:env_names.py" "lockbox-put:lockbox_put.py" "lockbox-names:lockbox_names.py"; do
|
||||||
n=${pair%%:*}; f=${pair##*:}
|
n=${pair%%:*}; f=${pair##*:}
|
||||||
printf '#!/usr/bin/env bash\nexec python3 "%s/%s" "$@"\n' "$dest" "$f" > "$HOME/.local/bin/$n"
|
printf '#!/usr/bin/env bash\nexec python3 "%s/%s" "$@"\n' "$dest" "$f" > "$HOME/.local/bin/$n"
|
||||||
chmod 755 "$HOME/.local/bin/$n"
|
chmod 755 "$HOME/.local/bin/$n"
|
||||||
done
|
done
|
||||||
echo "installed secret-scan, env-names and lockbox-put (shapes from secret_shapes.py, same as secret-guard)"
|
echo "installed secret-scan, env-names, lockbox-put and lockbox-names (shapes from secret_shapes.py, same as secret-guard)"
|
||||||
|
|||||||
134
scripts/lockbox_names.py
Normal file
134
scripts/lockbox_names.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""lockbox-names: DISCOVER what the lockbox holds without reading it (Boss rule 10-01).
|
||||||
|
|
||||||
|
lockbox-names [REGEX] (case-insensitive; matches the section heading or the label)
|
||||||
|
|
||||||
|
Prints one row per entry: SECTION HEADING | LABEL | kind | resolvable
|
||||||
|
kind env = `KEY=value` line md = `- **`KEY`**: `value`` line
|
||||||
|
label = a bold prose label (`**Password (X):** ...`) file = secrets/**/*.env key
|
||||||
|
resolvable yes = `lockbox-get LABEL FILE` returns exactly one value
|
||||||
|
dup = defined with 2+ different values (lockbox-get refuses)
|
||||||
|
no = a prose-only label, or not an exact key
|
||||||
|
|
||||||
|
NEVER prints a value or any prose after a label. A label or heading that itself looks
|
||||||
|
like a secret (secret_shapes) is replaced by <secret-shaped>. Reads the COMMITTED lockbox at
|
||||||
|
origin/main (like lockbox-get; LOCKBOX_REF=<ref> or WORKTREE overrides). Memory only, stdout only.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import secret_shapes as ss # noqa: E402
|
||||||
|
|
||||||
|
REPO = os.environ.get("LOCKBOX_REPO", os.path.expanduser("~/kit-army-config"))
|
||||||
|
REF = os.environ.get("LOCKBOX_REF", "origin/main")
|
||||||
|
HEAD = re.compile(r"^#{1,6}\s+(.*\S)\s*$")
|
||||||
|
ENV = re.compile(r"^([A-Z][A-Z0-9_]{2,})=(.*)$")
|
||||||
|
MD = re.compile(r"^\s*[-*]?\s*\*\*`([A-Za-z0-9_]+)`\*\*\s*:\s*`([^`]+)`")
|
||||||
|
LABEL = re.compile(r"\*\*([^*`]{2,70}?)\*\*")
|
||||||
|
|
||||||
|
|
||||||
|
def git(*a: str) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(["git", "-C", REPO, *a], capture_output=True, text=True, errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
def read_sources() -> dict[str, str]:
|
||||||
|
"""{path: text} for ACCESS_LOCKBOX.md and secrets/**/*.env."""
|
||||||
|
if REF == "WORKTREE":
|
||||||
|
out = {}
|
||||||
|
for p in [Path(REPO, "ACCESS_LOCKBOX.md"), *Path(REPO, "secrets").rglob("*.env")]:
|
||||||
|
if p.is_file():
|
||||||
|
out[str(p.relative_to(REPO))] = p.read_text(errors="ignore")
|
||||||
|
return out
|
||||||
|
if REF.startswith("origin/"):
|
||||||
|
git("fetch", "-q", "origin", REF.split("/", 1)[1])
|
||||||
|
names = ["ACCESS_LOCKBOX.md"] + [
|
||||||
|
p for p in git("ls-tree", "-r", "--name-only", REF, "--", "secrets").stdout.splitlines() if p.endswith(".env")]
|
||||||
|
out = {}
|
||||||
|
for n in names:
|
||||||
|
r = git("show", f"{REF}:{n}")
|
||||||
|
if r.returncode == 0:
|
||||||
|
out[n] = r.stdout
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def safe(text: str, limit: int = 70) -> str:
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
return "<secret-shaped>" if ss.find(text) else text[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def h(v: str) -> str:
|
||||||
|
return hashlib.sha256(v.strip().strip('"').strip("'").encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
def collect(src: dict[str, str]):
|
||||||
|
"""(rows, values) where values[KEY] = {hash,...} for resolvability; nothing printed from it."""
|
||||||
|
rows, values = [], {}
|
||||||
|
for path, text in src.items():
|
||||||
|
section = path
|
||||||
|
for line in text.splitlines():
|
||||||
|
m = HEAD.match(line) if path.endswith(".md") else None
|
||||||
|
if m:
|
||||||
|
section = safe(m.group(1), 90)
|
||||||
|
continue
|
||||||
|
m = ENV.match(line)
|
||||||
|
if m:
|
||||||
|
values.setdefault(m.group(1), set()).add(h(m.group(2)))
|
||||||
|
rows.append((section, m.group(1), "file" if path.startswith("secrets/") else "env"))
|
||||||
|
continue
|
||||||
|
m = MD.match(line)
|
||||||
|
if m:
|
||||||
|
values.setdefault(m.group(1), set()).add(h(m.group(2)))
|
||||||
|
rows.append((section, m.group(1), "md"))
|
||||||
|
continue
|
||||||
|
if path.endswith(".md"):
|
||||||
|
mm = LABEL.search(line)
|
||||||
|
if mm and not mm.group(1).startswith("http"):
|
||||||
|
rows.append((section, safe(mm.group(1)), "label"))
|
||||||
|
return rows, values
|
||||||
|
|
||||||
|
|
||||||
|
def resolvable(label: str, kind: str, values) -> str:
|
||||||
|
if kind not in ("env", "md", "file"):
|
||||||
|
return "no"
|
||||||
|
n = len(values.get(label, ()))
|
||||||
|
return "yes" if n == 1 else "dup" if n > 1 else "no"
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
argv = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
rx = re.compile(argv[0], re.I) if argv else None
|
||||||
|
src = read_sources()
|
||||||
|
if not src:
|
||||||
|
print("lockbox-names: cannot read the lockbox")
|
||||||
|
return 2
|
||||||
|
rows, values = collect(src)
|
||||||
|
seen, n = set(), 0
|
||||||
|
for section, label, kind in rows:
|
||||||
|
if rx and not (rx.search(section) or rx.search(label)):
|
||||||
|
continue
|
||||||
|
key = (section, label, kind)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
n += 1
|
||||||
|
print(f"{section} | {label} | {kind} | {resolvable(label, kind, values)}")
|
||||||
|
print(f"# {n} entr{'y' if n == 1 else 'ies'}; names only, values never printed")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except re.error:
|
||||||
|
print("lockbox-names: bad regex")
|
||||||
|
sys.exit(2)
|
||||||
|
except Exception as e: # never a traceback
|
||||||
|
print(f"lockbox-names: error: {type(e).__name__}")
|
||||||
|
sys.exit(2)
|
||||||
Reference in New Issue
Block a user