keywarden-ssh 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/PKG-INFO +11 -2
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/README.md +10 -1
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/pyproject.toml +1 -1
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/__init__.py +1 -1
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/cli.py +18 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/collect.py +28 -6
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/ui.py +14 -2
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/ui_page.py +16 -2
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden_ssh.egg-info/PKG-INFO +11 -2
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden_ssh.egg-info/SOURCES.txt +1 -0
- keywarden_ssh-0.3.1/tests/test_platform.py +54 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/LICENSE +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/setup.cfg +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/__main__.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/access.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/check.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/controls.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/evidence.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/inventory.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/parse.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/term.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/ui_lab.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden/waivers.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden_ssh.egg-info/dependency_links.txt +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden_ssh.egg-info/entry_points.txt +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/src/keywarden_ssh.egg-info/top_level.txt +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/tests/test_access.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/tests/test_evidence.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/tests/test_keywarden.py +0 -0
- {keywarden_ssh-0.3.0 → keywarden_ssh-0.3.1}/tests/test_ui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keywarden-ssh
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Check SSH posture around the public keys you push. Never stores them.
|
|
5
5
|
Author: Michael Waxman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,15 @@ One command checks this machine, one host, or every host in an Ansible inventory
|
|
|
31
31
|
|
|
32
32
|
`keywarden ui` on the built-in lab hosts. Screenshots: [findings, light](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/findings-light.webp) · [findings, dark](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/findings-dark.webp) · [access review](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/access-review.webp).
|
|
33
33
|
|
|
34
|
+
## Try it in a minute
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install keywarden-ssh
|
|
38
|
+
keywarden ui
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Your browser opens Keywarden. Press **Use Lab Keys**, then **Run Checks**, to see six made-up hosts with the kinds of problems real fleets have: a former employee's key on a shared admin account, a contractor key nobody uses, a root key that works from anywhere. Nothing is sent anywhere and nothing is stored. Then point it at a real host with **One host**.
|
|
42
|
+
|
|
34
43
|
## Install
|
|
35
44
|
|
|
36
45
|
Python 3.9 or newer. No third-party packages. `--local` needs Linux or another POSIX system; the browser UI, `--host`, and `--inventory` also run on Windows using its built-in `ssh`.
|
|
@@ -42,7 +51,7 @@ python3 -m pip install keywarden-ssh
|
|
|
42
51
|
Or straight from a tag:
|
|
43
52
|
|
|
44
53
|
```sh
|
|
45
|
-
python3 -m pip install "git+https://github.com/generalwax2018/keywarden.git@v0.3.
|
|
54
|
+
python3 -m pip install "git+https://github.com/generalwax2018/keywarden.git@v0.3.1"
|
|
46
55
|
```
|
|
47
56
|
|
|
48
57
|
The distribution name is `keywarden-ssh`. The `keywarden` package on PyPI is an unrelated project; do not install it expecting this tool.
|
|
@@ -10,6 +10,15 @@ One command checks this machine, one host, or every host in an Ansible inventory
|
|
|
10
10
|
|
|
11
11
|
`keywarden ui` on the built-in lab hosts. Screenshots: [findings, light](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/findings-light.webp) · [findings, dark](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/findings-dark.webp) · [access review](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/access-review.webp).
|
|
12
12
|
|
|
13
|
+
## Try it in a minute
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install keywarden-ssh
|
|
17
|
+
keywarden ui
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Your browser opens Keywarden. Press **Use Lab Keys**, then **Run Checks**, to see six made-up hosts with the kinds of problems real fleets have: a former employee's key on a shared admin account, a contractor key nobody uses, a root key that works from anywhere. Nothing is sent anywhere and nothing is stored. Then point it at a real host with **One host**.
|
|
21
|
+
|
|
13
22
|
## Install
|
|
14
23
|
|
|
15
24
|
Python 3.9 or newer. No third-party packages. `--local` needs Linux or another POSIX system; the browser UI, `--host`, and `--inventory` also run on Windows using its built-in `ssh`.
|
|
@@ -21,7 +30,7 @@ python3 -m pip install keywarden-ssh
|
|
|
21
30
|
Or straight from a tag:
|
|
22
31
|
|
|
23
32
|
```sh
|
|
24
|
-
python3 -m pip install "git+https://github.com/generalwax2018/keywarden.git@v0.3.
|
|
33
|
+
python3 -m pip install "git+https://github.com/generalwax2018/keywarden.git@v0.3.1"
|
|
25
34
|
```
|
|
26
35
|
|
|
27
36
|
The distribution name is `keywarden-ssh`. The `keywarden` package on PyPI is an unrelated project; do not install it expecting this tool.
|
|
@@ -7,6 +7,7 @@ import csv
|
|
|
7
7
|
import dataclasses
|
|
8
8
|
import io
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
import sys
|
|
11
12
|
from datetime import date
|
|
12
13
|
from pathlib import Path
|
|
@@ -609,6 +610,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
609
610
|
if args.inventory:
|
|
610
611
|
return _check_inventory(args)
|
|
611
612
|
return _check_one(args)
|
|
613
|
+
except BrokenPipeError:
|
|
614
|
+
# Output piped into head or similar that closed early; nothing went wrong.
|
|
615
|
+
try:
|
|
616
|
+
sys.stdout = open(os.devnull, "w")
|
|
617
|
+
except OSError:
|
|
618
|
+
pass
|
|
619
|
+
return 0
|
|
612
620
|
except PrivateKeyError:
|
|
613
621
|
print(
|
|
614
622
|
"Refusing to continue: a private key was in the input. Keywarden accepts public keys only and did not keep a copy.",
|
|
@@ -618,6 +626,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
618
626
|
except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
|
|
619
627
|
print(str(exc), file=sys.stderr)
|
|
620
628
|
return 2
|
|
629
|
+
except Exception as exc: # noqa: BLE001 - a bug should end in a message, not a traceback
|
|
630
|
+
if os.environ.get("KEYWARDEN_DEBUG"):
|
|
631
|
+
raise
|
|
632
|
+
print(
|
|
633
|
+
f"Keywarden hit an unexpected error ({type(exc).__name__}: {exc}). "
|
|
634
|
+
"Set KEYWARDEN_DEBUG=1 to see the details, and please report it at "
|
|
635
|
+
"https://github.com/generalwax2018/keywarden/issues",
|
|
636
|
+
file=sys.stderr,
|
|
637
|
+
)
|
|
638
|
+
return 3
|
|
621
639
|
|
|
622
640
|
|
|
623
641
|
if __name__ == "__main__":
|
|
@@ -336,6 +336,18 @@ echo END
|
|
|
336
336
|
"""
|
|
337
337
|
|
|
338
338
|
FINGERPRINT_TEXT = re.compile(r"SHA256:[A-Za-z0-9+/]+")
|
|
339
|
+
LOCAL_SUPPORTED = os.name == "posix" and hasattr(os, "geteuid")
|
|
340
|
+
LOCAL_UNSUPPORTED_MESSAGE = (
|
|
341
|
+
"--local reads this machine's sshd and user accounts, which only exist on Linux and other POSIX systems. "
|
|
342
|
+
"Run Keywarden on the server itself, or from here use --host user@server, --inventory hosts.ini, "
|
|
343
|
+
"or keywarden ui and its lab hosts."
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _euid() -> int:
|
|
348
|
+
return os.geteuid() if hasattr(os, "geteuid") else -1
|
|
349
|
+
|
|
350
|
+
|
|
339
351
|
_DESTINATION = re.compile(r"^[A-Za-z0-9._:-]+@[A-Za-z0-9._:-]+$|^[A-Za-z0-9._:-]+$")
|
|
340
352
|
_USERNAME = re.compile(r"^[a-z_][-a-z0-9_.]*\$?$", re.IGNORECASE)
|
|
341
353
|
|
|
@@ -446,7 +458,7 @@ def _run(command: list[str]) -> subprocess.CompletedProcess | None:
|
|
|
446
458
|
|
|
447
459
|
def _sshd_output(binary: str, extra: list[str]) -> str | None:
|
|
448
460
|
attempts = [[binary, "-T", *extra]]
|
|
449
|
-
if
|
|
461
|
+
if _euid() != 0 and shutil.which("sudo"):
|
|
450
462
|
attempts.append(["sudo", "-n", binary, "-T", *extra])
|
|
451
463
|
for command in attempts:
|
|
452
464
|
completed = _run(command)
|
|
@@ -619,6 +631,8 @@ def _run_long(command: list[str]) -> subprocess.CompletedProcess | None:
|
|
|
619
631
|
|
|
620
632
|
|
|
621
633
|
def collect_local(extra_users: list[str] | None = None) -> HostObservation:
|
|
634
|
+
if not LOCAL_SUPPORTED:
|
|
635
|
+
raise ValueError(LOCAL_UNSUPPORTED_MESSAGE)
|
|
622
636
|
records, passwd_state = _passwd_records()
|
|
623
637
|
accounts = []
|
|
624
638
|
for user in _ordered_users(extra_users or [], records):
|
|
@@ -652,7 +666,7 @@ def collect_local(extra_users: list[str] | None = None) -> HostObservation:
|
|
|
652
666
|
sshd_state=sshd_state,
|
|
653
667
|
match_blocks=match,
|
|
654
668
|
sshd_root=root,
|
|
655
|
-
privilege="root" if
|
|
669
|
+
privilege="root" if _euid() == 0 else "none",
|
|
656
670
|
now=now,
|
|
657
671
|
log_source=log_source,
|
|
658
672
|
log_since=log_since,
|
|
@@ -894,6 +908,12 @@ def _parse_account(lines: list[str], index: int) -> tuple[Account, int]:
|
|
|
894
908
|
return account, index
|
|
895
909
|
|
|
896
910
|
|
|
911
|
+
def _text(data) -> str:
|
|
912
|
+
if isinstance(data, bytes):
|
|
913
|
+
return data.decode("utf-8", errors="replace")
|
|
914
|
+
return data or ""
|
|
915
|
+
|
|
916
|
+
|
|
897
917
|
def collect_remote(
|
|
898
918
|
destination: str,
|
|
899
919
|
extra_users: list[str] | None = None,
|
|
@@ -933,11 +953,12 @@ def collect_remote(
|
|
|
933
953
|
options.extend(["-i", os.path.expanduser(identity_file), "-o", "IdentitiesOnly=yes"])
|
|
934
954
|
command = ["ssh", *options, "--", destination, "sh", "-s", "--", *users]
|
|
935
955
|
try:
|
|
956
|
+
# Bytes, not text: in text mode Windows would turn every newline in the
|
|
957
|
+
# script into CRLF on the way to the remote sh, and the script would fail.
|
|
936
958
|
completed = subprocess.run(
|
|
937
959
|
command,
|
|
938
|
-
input=REMOTE_SCRIPT,
|
|
960
|
+
input=REMOTE_SCRIPT.encode("utf-8"),
|
|
939
961
|
capture_output=True,
|
|
940
|
-
text=True,
|
|
941
962
|
timeout=60,
|
|
942
963
|
check=False,
|
|
943
964
|
)
|
|
@@ -945,14 +966,15 @@ def collect_remote(
|
|
|
945
966
|
raise RuntimeError("ssh timed out before a check could finish") from exc
|
|
946
967
|
except OSError as exc:
|
|
947
968
|
raise RuntimeError("ssh is not available on this machine") from exc
|
|
969
|
+
stderr = _text(completed.stderr)
|
|
948
970
|
if completed.returncode != 0:
|
|
949
971
|
tail = ""
|
|
950
|
-
for line in reversed(
|
|
972
|
+
for line in reversed(stderr.splitlines()):
|
|
951
973
|
if line.strip() and "PRIVATE KEY" not in line and "ssh-" not in line:
|
|
952
974
|
tail = line.strip()[:180]
|
|
953
975
|
break
|
|
954
976
|
raise RuntimeError(tail or f"ssh exited {completed.returncode}")
|
|
955
|
-
output = completed.stdout
|
|
977
|
+
output = _text(completed.stdout)
|
|
956
978
|
try:
|
|
957
979
|
return parse_protocol(output, address=destination)
|
|
958
980
|
finally:
|
|
@@ -32,7 +32,15 @@ from keywarden.cli import (
|
|
|
32
32
|
csv_text,
|
|
33
33
|
prior_from_document,
|
|
34
34
|
)
|
|
35
|
-
from keywarden.collect import
|
|
35
|
+
from keywarden.collect import (
|
|
36
|
+
LOCAL_SUPPORTED,
|
|
37
|
+
LOCAL_UNSUPPORTED_MESSAGE,
|
|
38
|
+
REMOTE_SCRIPT,
|
|
39
|
+
ProtocolError,
|
|
40
|
+
collect_local,
|
|
41
|
+
collect_remote,
|
|
42
|
+
parse_protocol,
|
|
43
|
+
)
|
|
36
44
|
from keywarden.parse import KeyRef, PrivateKeyError, kind_of, parse_public_keys
|
|
37
45
|
from keywarden.ui_lab import LAB_PUSH, lab_hosts
|
|
38
46
|
from keywarden.ui_page import render_page
|
|
@@ -99,6 +107,8 @@ def _observations(body: dict):
|
|
|
99
107
|
yield observation.name, observation, role, prior
|
|
100
108
|
return
|
|
101
109
|
if target == "local":
|
|
110
|
+
if not LOCAL_SUPPORTED:
|
|
111
|
+
raise BadRequest(LOCAL_UNSUPPORTED_MESSAGE.replace("--local", "This machine").replace(" or keywarden ui and its lab hosts", " or the lab hosts"))
|
|
102
112
|
observation = collect_local(args.account)
|
|
103
113
|
yield observation.name, observation, "This machine", None
|
|
104
114
|
return
|
|
@@ -276,7 +286,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
276
286
|
self._headers(403, "text/plain; charset=utf-8", len(body))
|
|
277
287
|
return self.wfile.write(body)
|
|
278
288
|
nonce = secrets.token_urlsafe(16)
|
|
279
|
-
page = render_page(nonce, self.server.token, __version__).encode("utf-8")
|
|
289
|
+
page = render_page(nonce, self.server.token, __version__, LOCAL_SUPPORTED).encode("utf-8")
|
|
280
290
|
self._headers(200, "text/html; charset=utf-8", len(page), nonce)
|
|
281
291
|
return self.wfile.write(page)
|
|
282
292
|
return self._send_json(404, {"error": "not found"})
|
|
@@ -317,6 +327,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
317
327
|
return self._send_json(400, {"error": "A private key was in the input. Keywarden takes public keys only and kept nothing."})
|
|
318
328
|
except (BadRequest, ValueError, OSError, RuntimeError, json.JSONDecodeError) as exc:
|
|
319
329
|
return self._send_json(400, {"error": str(exc)[:400]})
|
|
330
|
+
except Exception as exc: # noqa: BLE001 - answer the page instead of dropping the connection
|
|
331
|
+
return self._send_json(500, {"error": f"Keywarden hit an unexpected error ({type(exc).__name__}). Please report it on GitHub."})
|
|
320
332
|
finally:
|
|
321
333
|
body.clear()
|
|
322
334
|
|
|
@@ -119,6 +119,7 @@ textarea:focus, input[type=text]:focus, input[type=number]:focus { outline: none
|
|
|
119
119
|
.option .text { display: grid; gap: 1px; min-width: 0; }
|
|
120
120
|
.option small { color: var(--secondary); font-size: 12px; }
|
|
121
121
|
.option .sub { grid-column: 1 / -1; margin-top: 6px; }
|
|
122
|
+
.option.disabled { opacity: .5; cursor: not-allowed; }
|
|
122
123
|
.option input[type=radio] { appearance: none; -webkit-appearance: none; width: 16px; height: 16px; margin: 0; border-radius: 50%; border: 1.5px solid var(--separator-strong); background: var(--glass-strong); }
|
|
123
124
|
.option input[type=radio]:checked { border: 5px solid var(--blue); background: #fff; }
|
|
124
125
|
.option input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 36px; height: 22px; border-radius: 11px; background: var(--fill-strong); position: relative; margin: 0; cursor: pointer; transition: background .2s; flex: none; box-shadow: inset 0 1px 2px rgba(0,0,0,.12); }
|
|
@@ -338,6 +339,7 @@ td.fp, td.mono { font-family: var(--mono); font-size: 12px; }
|
|
|
338
339
|
<script nonce="__NONCE__">
|
|
339
340
|
"use strict";
|
|
340
341
|
const TOKEN = "__TOKEN__";
|
|
342
|
+
const LOCAL_OK = __LOCAL_OK__;
|
|
341
343
|
history.replaceState(null, "", "/");
|
|
342
344
|
|
|
343
345
|
const $ = (id) => document.getElementById(id);
|
|
@@ -654,6 +656,13 @@ $("prior").addEventListener("change", async (event) => {
|
|
|
654
656
|
});
|
|
655
657
|
for (const tab of document.querySelectorAll('[role="tab"]')) tab.addEventListener("click", () => showView(tab.dataset.view));
|
|
656
658
|
for (const id of ["host", "inventory"]) $(id).addEventListener("focus", () => { $(id === "host" ? "t-host" : "t-inv").checked = true; });
|
|
659
|
+
if (!LOCAL_OK) {
|
|
660
|
+
// sshd and the account database live on the server; a Windows desktop has neither.
|
|
661
|
+
$("t-local").disabled = true;
|
|
662
|
+
const row = $("t-local").closest(".option");
|
|
663
|
+
row.classList.add("disabled");
|
|
664
|
+
row.querySelector("small").textContent = "Not on Windows. Run Keywarden on the Linux server, or check it as One host.";
|
|
665
|
+
}
|
|
657
666
|
renderPush(); renderAll([]);
|
|
658
667
|
</script>
|
|
659
668
|
</body>
|
|
@@ -661,5 +670,10 @@ renderPush(); renderAll([]);
|
|
|
661
670
|
"""
|
|
662
671
|
|
|
663
672
|
|
|
664
|
-
def render_page(nonce: str, token: str, version: str) -> str:
|
|
665
|
-
return
|
|
673
|
+
def render_page(nonce: str, token: str, version: str, local_ok: bool = True) -> str:
|
|
674
|
+
return (
|
|
675
|
+
PAGE.replace("__NONCE__", nonce)
|
|
676
|
+
.replace("__TOKEN__", token)
|
|
677
|
+
.replace("__VERSION__", version)
|
|
678
|
+
.replace("__LOCAL_OK__", "true" if local_ok else "false")
|
|
679
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keywarden-ssh
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Check SSH posture around the public keys you push. Never stores them.
|
|
5
5
|
Author: Michael Waxman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,15 @@ One command checks this machine, one host, or every host in an Ansible inventory
|
|
|
31
31
|
|
|
32
32
|
`keywarden ui` on the built-in lab hosts. Screenshots: [findings, light](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/findings-light.webp) · [findings, dark](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/findings-dark.webp) · [access review](https://raw.githubusercontent.com/generalwax2018/keywarden/main/docs/screenshots/access-review.webp).
|
|
33
33
|
|
|
34
|
+
## Try it in a minute
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install keywarden-ssh
|
|
38
|
+
keywarden ui
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Your browser opens Keywarden. Press **Use Lab Keys**, then **Run Checks**, to see six made-up hosts with the kinds of problems real fleets have: a former employee's key on a shared admin account, a contractor key nobody uses, a root key that works from anywhere. Nothing is sent anywhere and nothing is stored. Then point it at a real host with **One host**.
|
|
42
|
+
|
|
34
43
|
## Install
|
|
35
44
|
|
|
36
45
|
Python 3.9 or newer. No third-party packages. `--local` needs Linux or another POSIX system; the browser UI, `--host`, and `--inventory` also run on Windows using its built-in `ssh`.
|
|
@@ -42,7 +51,7 @@ python3 -m pip install keywarden-ssh
|
|
|
42
51
|
Or straight from a tag:
|
|
43
52
|
|
|
44
53
|
```sh
|
|
45
|
-
python3 -m pip install "git+https://github.com/generalwax2018/keywarden.git@v0.3.
|
|
54
|
+
python3 -m pip install "git+https://github.com/generalwax2018/keywarden.git@v0.3.1"
|
|
46
55
|
```
|
|
47
56
|
|
|
48
57
|
The distribution name is `keywarden-ssh`. The `keywarden` package on PyPI is an unrelated project; do not install it expecting this tool.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
11
|
+
|
|
12
|
+
from keywarden import collect # noqa: E402
|
|
13
|
+
from keywarden.cli import main # noqa: E402
|
|
14
|
+
from keywarden.ui_page import render_page # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WindowsTests(unittest.TestCase):
|
|
18
|
+
def test_local_check_explains_itself_off_posix(self):
|
|
19
|
+
with patch.object(collect, "LOCAL_SUPPORTED", False):
|
|
20
|
+
with self.assertRaises(ValueError) as caught:
|
|
21
|
+
collect.collect_local()
|
|
22
|
+
self.assertIn("--host", str(caught.exception))
|
|
23
|
+
err = StringIO()
|
|
24
|
+
with redirect_stderr(err), redirect_stdout(StringIO()):
|
|
25
|
+
code = main(["check", "--local"])
|
|
26
|
+
self.assertEqual(code, 2)
|
|
27
|
+
self.assertIn("Linux", err.getvalue())
|
|
28
|
+
self.assertNotIn("Traceback", err.getvalue())
|
|
29
|
+
|
|
30
|
+
def test_ui_disables_this_machine_off_posix(self):
|
|
31
|
+
self.assertIn("const LOCAL_OK = false;", render_page("n", "t", "0", False))
|
|
32
|
+
self.assertIn("const LOCAL_OK = true;", render_page("n", "t", "0", True))
|
|
33
|
+
|
|
34
|
+
def test_script_is_sent_as_bytes_so_newlines_survive_windows(self):
|
|
35
|
+
with patch("keywarden.collect.subprocess.run") as run:
|
|
36
|
+
run.return_value = subprocess.CompletedProcess([], 255, b"", b"ssh: connect to host x port 22: refused\r\n")
|
|
37
|
+
with self.assertRaises(RuntimeError) as caught:
|
|
38
|
+
collect.collect_remote("deploy@x")
|
|
39
|
+
sent = run.call_args.kwargs["input"]
|
|
40
|
+
self.assertIsInstance(sent, bytes)
|
|
41
|
+
self.assertNotIn(b"\r\n", sent)
|
|
42
|
+
self.assertNotIn("text", run.call_args.kwargs)
|
|
43
|
+
self.assertIn("refused", str(caught.exception))
|
|
44
|
+
|
|
45
|
+
def test_unexpected_errors_end_in_a_message(self):
|
|
46
|
+
err = StringIO()
|
|
47
|
+
with patch("keywarden.cli._check_one", side_effect=KeyError("boom")), redirect_stderr(err):
|
|
48
|
+
code = main(["check", "--local"])
|
|
49
|
+
self.assertEqual(code, 3)
|
|
50
|
+
self.assertIn("unexpected error", err.getvalue())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|