gotchu 0.1.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.
- gotchu/__init__.py +3 -0
- gotchu/__main__.py +219 -0
- gotchu/admin.py +156 -0
- gotchu/browse.ps1 +15 -0
- gotchu/check.py +307 -0
- gotchu/collect.ps1 +69 -0
- gotchu/exposure.py +200 -0
- gotchu/harden.ps1 +28 -0
- gotchu/harden.py +162 -0
- gotchu/harden_apply.ps1 +80 -0
- gotchu/pe.py +79 -0
- gotchu/report.py +47 -0
- gotchu/respond.py +118 -0
- gotchu/scan.py +220 -0
- gotchu/secrets_map.json +49 -0
- gotchu/tripwire.py +364 -0
- gotchu/tripwire_apply.ps1 +119 -0
- gotchu/tripwire_watch.ps1 +169 -0
- gotchu/ui.html +379 -0
- gotchu/ui.py +124 -0
- gotchu/vault.py +279 -0
- gotchu-0.1.0.dist-info/METADATA +99 -0
- gotchu-0.1.0.dist-info/RECORD +27 -0
- gotchu-0.1.0.dist-info/WHEEL +5 -0
- gotchu-0.1.0.dist-info/entry_points.txt +2 -0
- gotchu-0.1.0.dist-info/licenses/LICENSE +202 -0
- gotchu-0.1.0.dist-info/top_level.txt +1 -0
gotchu/__init__.py
ADDED
gotchu/__main__.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import __version__, check, exposure, harden, report, respond, scan, tripwire
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _choose_passphrase(getpass, prompt, nothing):
|
|
10
|
+
"""Ask for a new passphrase twice. None (after saying why) if it is too short or the two differ."""
|
|
11
|
+
passphrase = getpass.getpass(f"{prompt} (at least 10 characters; a few random words work well): ")
|
|
12
|
+
if len(passphrase) < 10:
|
|
13
|
+
print(f"Too short. {nothing}")
|
|
14
|
+
return None
|
|
15
|
+
if passphrase != getpass.getpass("Type it again: "):
|
|
16
|
+
print(f"The two did not match. {nothing}")
|
|
17
|
+
return None
|
|
18
|
+
return passphrase
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
RECOVERY_NOTE = ("Write this on paper now and keep it away from this PC. It is shown only once.\n"
|
|
22
|
+
"If you forget the passphrase, this key is the only other way in. Lose both and the\n"
|
|
23
|
+
"files in the vault are gone for good.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _vault(args):
|
|
27
|
+
import getpass
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
from . import vault
|
|
31
|
+
try:
|
|
32
|
+
if args.action == "create":
|
|
33
|
+
if vault.exists():
|
|
34
|
+
print("There is already a vault on this PC.")
|
|
35
|
+
return 1
|
|
36
|
+
passphrase = _choose_passphrase(getpass, "Choose a passphrase", "Nothing was created.")
|
|
37
|
+
if passphrase is None:
|
|
38
|
+
return 1
|
|
39
|
+
recovery = vault.create(passphrase)
|
|
40
|
+
print(f"\nVault created in {vault.FOLDER}\n\nRECOVERY KEY: {recovery}\n\n{RECOVERY_NOTE}")
|
|
41
|
+
return 0
|
|
42
|
+
if args.action == "restore":
|
|
43
|
+
vault.restore(args.folder)
|
|
44
|
+
print(f"Vault restored from {args.folder}. It opens with the passphrase or recovery key it had when it was backed up.")
|
|
45
|
+
return 0
|
|
46
|
+
if not vault.exists():
|
|
47
|
+
print("There is no vault yet. Make one with: gotchu vault create")
|
|
48
|
+
return 1
|
|
49
|
+
if args.action == "backup":
|
|
50
|
+
count = vault.backup(args.folder)
|
|
51
|
+
print(f"Copied the vault ({count} files, still encrypted) to {args.folder}.\n"
|
|
52
|
+
"The copy is useless without your passphrase or recovery key, so it is safe on a USB stick or in cloud storage.")
|
|
53
|
+
return 0
|
|
54
|
+
key = vault.unlock(getpass.getpass("Vault passphrase (or recovery key): "))
|
|
55
|
+
if args.action == "list":
|
|
56
|
+
print("\n".join(sorted(vault.names(key))) or "The vault is empty.")
|
|
57
|
+
elif args.action == "add":
|
|
58
|
+
for path in args.paths:
|
|
59
|
+
name = vault.add(key, path, name=args.name if len(args.paths) == 1 else None, move=args.move)
|
|
60
|
+
print(f"{name}: in the vault" + (", original deleted." if args.move else
|
|
61
|
+
f". The original is still at {path} - delete it, or add with --move."))
|
|
62
|
+
if args.move:
|
|
63
|
+
print("A deleted file can sometimes be recovered from the disk. If a key was ever exposed, replace it.")
|
|
64
|
+
elif args.action == "get":
|
|
65
|
+
target = args.to or args.name
|
|
66
|
+
if os.path.exists(target):
|
|
67
|
+
print(f"{target} already exists. Nothing was written.")
|
|
68
|
+
return 1
|
|
69
|
+
with open(target, "wb") as f:
|
|
70
|
+
f.write(vault.get(key, args.name))
|
|
71
|
+
print(f"Written to {target}. It is an ordinary file again - delete it when you are done.")
|
|
72
|
+
elif args.action == "use":
|
|
73
|
+
command = [part for part in args.run if part != "--"]
|
|
74
|
+
if not command:
|
|
75
|
+
print("Give the command to run, for example: gotchu vault use server.pem -- ssh -i {} user@host")
|
|
76
|
+
return 1
|
|
77
|
+
return vault.use(key, args.name, command)
|
|
78
|
+
elif args.action == "passphrase":
|
|
79
|
+
passphrase = _choose_passphrase(getpass, "Choose a new passphrase", "Nothing was changed.")
|
|
80
|
+
if passphrase is None:
|
|
81
|
+
return 1
|
|
82
|
+
vault.change_passphrase(key, passphrase)
|
|
83
|
+
print("Passphrase changed. The old one no longer opens the vault; your recovery key still does.")
|
|
84
|
+
elif args.action == "recovery":
|
|
85
|
+
recovery = vault.replace_recovery(key)
|
|
86
|
+
print(f"\nNEW RECOVERY KEY: {recovery}\n\nThe old recovery key no longer opens the vault.\n{RECOVERY_NOTE}")
|
|
87
|
+
else:
|
|
88
|
+
vault.remove(key, args.name)
|
|
89
|
+
print(f"{args.name} removed from the vault.")
|
|
90
|
+
return 0
|
|
91
|
+
except vault.WrongSecret:
|
|
92
|
+
print("That did not open the vault. Nothing was changed.")
|
|
93
|
+
except KeyError as e:
|
|
94
|
+
print(e.args[0])
|
|
95
|
+
except (OSError, ValueError) as e:
|
|
96
|
+
print(e)
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _setup(args):
|
|
101
|
+
from . import admin
|
|
102
|
+
ok, message = admin.remove() if args.remove else admin.install()
|
|
103
|
+
print(message)
|
|
104
|
+
if ok and not args.remove:
|
|
105
|
+
if admin.own_folder_writable():
|
|
106
|
+
print(f"\nNote: gotchu itself is in a folder your account can change ({admin.HERE}). A program running as\n"
|
|
107
|
+
"you could change it, including what it asks Windows to run as administrator. For the strongest\n"
|
|
108
|
+
"setup, install gotchu with a Python that is installed for all users, in Program Files.")
|
|
109
|
+
if tripwire.status()["on"]:
|
|
110
|
+
print("\nThe tripwire is on: run gotchu tripwire --on once more so its watcher uses the new version.")
|
|
111
|
+
return 0 if ok else 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv=None):
|
|
115
|
+
parser = argparse.ArgumentParser(prog="gotchu", description="Free, local, read-only PC safety checks.")
|
|
116
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
117
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
118
|
+
p = sub.add_parser("check", help="inspect a download (zip, folder or file) without running it")
|
|
119
|
+
p.add_argument("path")
|
|
120
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
121
|
+
p = sub.add_parser("scan", help="look for signs that this PC is infected right now")
|
|
122
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
123
|
+
p = sub.add_parser("exposure", help="show what a thief could take from this PC right now (counts only)")
|
|
124
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
125
|
+
p = sub.add_parser("harden", help="show which Windows protections are on; turn one on or put it back")
|
|
126
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
127
|
+
changeable = [k["key"] for k in harden.LOCKS if k["can_apply"]]
|
|
128
|
+
p.add_argument("--on", choices=changeable, metavar="LOCK", help="turn on: " + ", ".join(changeable))
|
|
129
|
+
p.add_argument("--undo", choices=changeable, metavar="LOCK", help="put a lock back the way it was")
|
|
130
|
+
p = sub.add_parser("tripwire", help="decoy files that raise an alarm when a thief reads them")
|
|
131
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
132
|
+
p.add_argument("--on", action="store_true", help="plant the decoys and start watching (alert only)")
|
|
133
|
+
p.add_argument("--cut", action="store_true", help="with --on: also block the reading program from the internet")
|
|
134
|
+
p.add_argument("--off", action="store_true", help="remove the decoys and put Windows back as it was")
|
|
135
|
+
p.add_argument("--restore", action="store_true", help="after an alarm: lift the internet blocks and clear the alarm")
|
|
136
|
+
p = sub.add_parser("vault", help="keep key files encrypted so a stolen copy is useless")
|
|
137
|
+
act = p.add_subparsers(dest="action", required=True)
|
|
138
|
+
act.add_parser("create", help="make the vault and choose its passphrase")
|
|
139
|
+
a = act.add_parser("add", help="encrypt files into the vault")
|
|
140
|
+
a.add_argument("paths", nargs="+")
|
|
141
|
+
a.add_argument("--move", action="store_true", help="delete the original once the vault copy is verified")
|
|
142
|
+
a.add_argument("--name", help="store a single file under another name")
|
|
143
|
+
act.add_parser("list", help="show what is in the vault")
|
|
144
|
+
a = act.add_parser("get", help="take a file out as an ordinary file")
|
|
145
|
+
a.add_argument("name")
|
|
146
|
+
a.add_argument("--to", help="where to write it (default: this folder)")
|
|
147
|
+
a = act.add_parser("use", help="run a command with the file present only while it runs")
|
|
148
|
+
a.add_argument("name")
|
|
149
|
+
a.add_argument("run", nargs=argparse.REMAINDER, help="the command; {} is replaced by the file's path")
|
|
150
|
+
a = act.add_parser("remove", help="delete a file from the vault")
|
|
151
|
+
a.add_argument("name")
|
|
152
|
+
act.add_parser("passphrase", help="choose a new passphrase (opens with the old one or the recovery key)")
|
|
153
|
+
act.add_parser("recovery", help="make a new recovery key; the old one stops working")
|
|
154
|
+
a = act.add_parser("backup", help="copy the vault, still encrypted, to another folder or drive")
|
|
155
|
+
a.add_argument("folder", help="a new or empty folder")
|
|
156
|
+
a = act.add_parser("restore", help="bring a backed-up vault back onto a PC that has none")
|
|
157
|
+
a.add_argument("folder", help="the folder a backup was copied to")
|
|
158
|
+
p = sub.add_parser("respond", help="after running something bad: what to do, in what order")
|
|
159
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
160
|
+
p.add_argument("--sites", action="store_true", help="also list the websites with a password saved on this PC (names only)")
|
|
161
|
+
p = sub.add_parser("setup", help="install or update gotchu's administrator helpers in Program Files")
|
|
162
|
+
p.add_argument("--remove", action="store_true", help="remove them, once nothing gotchu turned on still needs them")
|
|
163
|
+
p = sub.add_parser("ui", help="open the gotchu window in your browser")
|
|
164
|
+
p.add_argument("path", nargs="?", help="a download to check as soon as the window opens")
|
|
165
|
+
p.add_argument("--no-browser", action="store_true", help="start without opening a browser")
|
|
166
|
+
args = parser.parse_args(argv)
|
|
167
|
+
|
|
168
|
+
if args.command == "ui":
|
|
169
|
+
from . import ui
|
|
170
|
+
return ui.serve(open_browser=not args.no_browser, path=args.path)
|
|
171
|
+
|
|
172
|
+
if args.command == "vault":
|
|
173
|
+
return _vault(args)
|
|
174
|
+
|
|
175
|
+
if args.command == "setup":
|
|
176
|
+
return _setup(args)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
return _run(args)
|
|
180
|
+
except report.CouldNotRun as e:
|
|
181
|
+
print(e)
|
|
182
|
+
return 3
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _run(args):
|
|
186
|
+
if args.command == "harden" and (args.on or args.undo):
|
|
187
|
+
ok, message = harden.change(args.on or args.undo, undo=bool(args.undo))
|
|
188
|
+
print(message)
|
|
189
|
+
return 0 if ok else 1
|
|
190
|
+
|
|
191
|
+
if args.command == "tripwire" and (args.on or args.off or args.restore):
|
|
192
|
+
ok, message = tripwire.change("on" if args.on else "off" if args.off else "restore", cut=args.cut)
|
|
193
|
+
print(message)
|
|
194
|
+
return 0 if ok else 1
|
|
195
|
+
|
|
196
|
+
if args.command == "check":
|
|
197
|
+
if not os.path.exists(args.path):
|
|
198
|
+
raise report.CouldNotRun(f"{args.path} does not exist. Check the path and try again.")
|
|
199
|
+
try:
|
|
200
|
+
result = check.result(args.path)
|
|
201
|
+
except OSError as e:
|
|
202
|
+
raise report.CouldNotRun(f"Windows would not let gotchu read {args.path} ({e.strerror or e}). "
|
|
203
|
+
"Try a copy in your Downloads folder.") from None
|
|
204
|
+
elif args.command == "tripwire":
|
|
205
|
+
result = tripwire.result()
|
|
206
|
+
elif args.command == "exposure":
|
|
207
|
+
result = exposure.result()
|
|
208
|
+
elif args.command == "respond":
|
|
209
|
+
result = respond.result(with_sites=args.sites)
|
|
210
|
+
else:
|
|
211
|
+
result = scan.result() if args.command == "scan" else harden.result()
|
|
212
|
+
print(json.dumps(result, indent=2) if args.json else report.render(result))
|
|
213
|
+
if args.command == "respond" and args.sites and not args.json:
|
|
214
|
+
print(f"\nWebsites with a password saved on this PC ({len(result['sites'])}):\n" + "\n".join(result["sites"]))
|
|
215
|
+
return {"danger": 2, "warn": 1}.get(result["level"], 0)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
sys.exit(main())
|
gotchu/admin.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Administrator helpers: the only gotchu code that runs with administrator rights.
|
|
2
|
+
|
|
3
|
+
They run from C:\\Program Files\\gotchu, which only an administrator can change - never from gotchu's
|
|
4
|
+
own folder, which is often one that any program running as you can change.
|
|
5
|
+
|
|
6
|
+
They are copied there the first time they are needed, or by `gotchu setup` after an update. After
|
|
7
|
+
that, gotchu will not ask Windows to elevate while its own copy differs from the installed one, so a
|
|
8
|
+
program that edits gotchu's files cannot slip its code into a permission prompt you were expecting.
|
|
9
|
+
|
|
10
|
+
Limit, stated plainly: if gotchu's own folder is one you can change, a program running as you could
|
|
11
|
+
change gotchu itself. `gotchu setup` says so; installing for all users, in Program Files, closes it.
|
|
12
|
+
"""
|
|
13
|
+
import hashlib
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
HERE = os.path.dirname(__file__)
|
|
19
|
+
INSTALLED = os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"), "gotchu")
|
|
20
|
+
HELPERS = ("harden_apply.ps1", "tripwire_apply.ps1", "tripwire_watch.ps1")
|
|
21
|
+
CANCELLED = 1223 # Windows' code for "the user said no"
|
|
22
|
+
UNANSWERED = -1 # ours: the prompt sat unanswered until gotchu stopped waiting
|
|
23
|
+
DECLINED = "Nothing was changed: the Windows permission prompt was declined or closed."
|
|
24
|
+
NO_ANSWER = ("Nothing was changed: the Windows permission prompt was not answered within five minutes. It may be "
|
|
25
|
+
"behind another window or flashing on the taskbar. If it is still on screen, choose No, then try again.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sha256(path):
|
|
29
|
+
with open(path, "rb") as f:
|
|
30
|
+
return hashlib.sha256(f.read()).hexdigest().upper()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def state(source=HERE, dest=INSTALLED):
|
|
34
|
+
"""'current', 'missing' (not installed yet) or 'different' (installed, but not this copy's version)."""
|
|
35
|
+
missing = False
|
|
36
|
+
for name in HELPERS:
|
|
37
|
+
try:
|
|
38
|
+
with open(os.path.join(dest, name), "rb") as f:
|
|
39
|
+
installed = f.read()
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
missing = True
|
|
42
|
+
continue
|
|
43
|
+
with open(os.path.join(source, name), "rb") as f:
|
|
44
|
+
if f.read() != installed:
|
|
45
|
+
return "different"
|
|
46
|
+
return "missing" if missing else "current"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def own_folder_writable(folder=HERE):
|
|
50
|
+
"""Can this account change gotchu's own files? Then so can any program it runs."""
|
|
51
|
+
try:
|
|
52
|
+
with tempfile.TemporaryFile(dir=folder):
|
|
53
|
+
return True
|
|
54
|
+
except OSError:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _quote(text):
|
|
59
|
+
return "'" + text.replace("'", "''") + "'"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def launcher(args, elevate=True):
|
|
63
|
+
"""PowerShell that starts powershell.exe with `args` - with administrator rights, after Windows asks -
|
|
64
|
+
waits for it, and exits with its exit code (CANCELLED if the prompt was declined)."""
|
|
65
|
+
verb = "-Verb RunAs " if elevate else ""
|
|
66
|
+
listed = ",".join(_quote(a) for a in args)
|
|
67
|
+
return (f"try {{ $p = Start-Process powershell.exe {verb}-Wait -PassThru -WindowStyle Hidden -ArgumentList {listed} }} "
|
|
68
|
+
f"catch {{ exit {CANCELLED} }}; if ($null -eq $p.ExitCode) {{ exit 1 }}; exit $p.ExitCode")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _elevate(args):
|
|
72
|
+
try:
|
|
73
|
+
out = subprocess.run(["powershell.exe", "-NoProfile", "-Command", launcher(args)], capture_output=True, timeout=300)
|
|
74
|
+
except subprocess.TimeoutExpired:
|
|
75
|
+
return UNANSWERED
|
|
76
|
+
return out.returncode
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _refused(code):
|
|
80
|
+
"""The message for a prompt that was declined or left unanswered; None for any other exit code."""
|
|
81
|
+
return {CANCELLED: DECLINED, UNANSWERED: NO_ANSWER}.get(code)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def install_script(hashes, source=HERE, dest=INSTALLED):
|
|
85
|
+
"""PowerShell, run elevated, that copies the helpers into dest. A copy is kept only if it is exactly the
|
|
86
|
+
file gotchu read (same SHA-256), so a file swapped in the meantime never stays in Program Files."""
|
|
87
|
+
pairs = ",".join(f"@({_quote(name)},{_quote(digest)})" for name, digest in hashes.items())
|
|
88
|
+
return (f"$ErrorActionPreference = 'Stop'; $d = {_quote(dest)}; New-Item -ItemType Directory -Force -Path $d | Out-Null; "
|
|
89
|
+
f"foreach ($f in @({pairs})) {{ $to = Join-Path $d $f[0]; "
|
|
90
|
+
f"Copy-Item -LiteralPath (Join-Path {_quote(source)} $f[0]) -Destination $to -Force; "
|
|
91
|
+
"if ((Get-FileHash -LiteralPath $to -Algorithm SHA256).Hash -ne $f[1]) "
|
|
92
|
+
"{ Remove-Item -LiteralPath $to -Force; exit 2 } }; exit 0")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def install(source=HERE, dest=INSTALLED):
|
|
96
|
+
"""Copy the helpers into Program Files: one Windows permission prompt. Returns (ok, message)."""
|
|
97
|
+
if state(source, dest) == "current":
|
|
98
|
+
return True, f"GotchU's administrator helpers in {dest} are installed and up to date."
|
|
99
|
+
hashes = {name: _sha256(os.path.join(source, name)) for name in HELPERS}
|
|
100
|
+
script = install_script(hashes, source, dest)
|
|
101
|
+
code = _elevate(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", f'"{script}"'])
|
|
102
|
+
if _refused(code):
|
|
103
|
+
return False, _refused(code)
|
|
104
|
+
if code == 2:
|
|
105
|
+
return False, ("A helper file changed while it was being installed, so it was removed again. "
|
|
106
|
+
"Run gotchu scan, then try again.")
|
|
107
|
+
if code or state(source, dest) != "current":
|
|
108
|
+
return False, f"Windows could not install GotchU's administrator helpers in {dest}."
|
|
109
|
+
return True, f"Installed GotchU's administrator helpers in {dest}."
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def remove(dest=INSTALLED):
|
|
113
|
+
"""Remove the helpers, but only when nothing gotchu set up still needs them. Returns (ok, message)."""
|
|
114
|
+
if os.path.exists(os.path.join(dest, "tripwire.json")):
|
|
115
|
+
return False, "The tripwire is on and uses these helpers. Turn it off first: gotchu tripwire --off"
|
|
116
|
+
journal = os.path.join(dest, "journal")
|
|
117
|
+
pending = sorted(os.path.splitext(n)[0] for n in os.listdir(journal)) if os.path.isdir(journal) else []
|
|
118
|
+
if pending:
|
|
119
|
+
return False, ("These helpers are needed to put back what GotchU turned on: " + ", ".join(pending)
|
|
120
|
+
+ ". Undo those first with gotchu harden --undo <lock>.")
|
|
121
|
+
if not os.path.isdir(dest):
|
|
122
|
+
return True, "GotchU's administrator helpers are not installed."
|
|
123
|
+
code = _elevate(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
|
124
|
+
f'"Remove-Item -LiteralPath {_quote(dest)} -Recurse -Force"'])
|
|
125
|
+
if _refused(code):
|
|
126
|
+
return False, _refused(code)
|
|
127
|
+
if os.path.isdir(dest):
|
|
128
|
+
return False, f"Windows could not remove {dest}."
|
|
129
|
+
return True, f"Removed {dest}."
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run(script, args):
|
|
133
|
+
"""Run one helper from Program Files with administrator rights; Windows asks the user first.
|
|
134
|
+
|
|
135
|
+
The first time, the helpers are installed before that (one more prompt). Returns (ok, message).
|
|
136
|
+
"""
|
|
137
|
+
current = state()
|
|
138
|
+
if current == "different":
|
|
139
|
+
return False, (f"GotchU's administrator helpers in {INSTALLED} are not the ones in this copy of GotchU, "
|
|
140
|
+
"so nothing was changed. If you have just updated GotchU, run: gotchu setup. "
|
|
141
|
+
"If you have not, something may have changed GotchU's files: run gotchu scan first.")
|
|
142
|
+
if current == "missing":
|
|
143
|
+
ok, message = install()
|
|
144
|
+
if not ok:
|
|
145
|
+
return False, message
|
|
146
|
+
code = _elevate(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", f'"{os.path.join(INSTALLED, script)}"']
|
|
147
|
+
+ list(args))
|
|
148
|
+
if _refused(code):
|
|
149
|
+
return False, _refused(code)
|
|
150
|
+
if code:
|
|
151
|
+
try:
|
|
152
|
+
with open(os.path.join(INSTALLED, "last-error.txt"), encoding="utf-8-sig") as f:
|
|
153
|
+
return False, "Windows refused the change. " + f.read().strip()
|
|
154
|
+
except OSError:
|
|
155
|
+
return False, f"Windows refused the change (code {code})."
|
|
156
|
+
return True, ""
|
gotchu/browse.ps1
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Shows the standard Windows "pick a file / folder" window and prints the chosen path.
|
|
2
|
+
param([switch]$Folder)
|
|
3
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
4
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
5
|
+
$owner = New-Object System.Windows.Forms.Form -Property @{ TopMost = $true; ShowInTaskbar = $false; Opacity = 0 }
|
|
6
|
+
$owner.Show()
|
|
7
|
+
if ($Folder) {
|
|
8
|
+
$d = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
9
|
+
if ($d.ShowDialog($owner) -eq 'OK') { $d.SelectedPath }
|
|
10
|
+
} else {
|
|
11
|
+
$d = New-Object System.Windows.Forms.OpenFileDialog
|
|
12
|
+
$d.InitialDirectory = Join-Path $env:USERPROFILE 'Downloads'
|
|
13
|
+
if ($d.ShowDialog($owner) -eq 'OK') { $d.FileName }
|
|
14
|
+
}
|
|
15
|
+
$owner.Close()
|