witbitz-code 1.6.10__tar.gz → 1.7.0__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.
- {witbitz_code-1.6.10/src/witbitz_code.egg-info → witbitz_code-1.7.0}/PKG-INFO +1 -1
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/pyproject.toml +1 -1
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/connector.py +11 -0
- witbitz_code-1.7.0/src/witbitz_code/seccheck.py +226 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/ui.py +6 -3
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/ui_static/app.css +8 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/ui_static/app.js +41 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/ui_static/index.html +8 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/SOURCES.txt +2 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_connector_e2e.py +23 -0
- witbitz_code-1.7.0/tests/test_seccheck.py +143 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/LICENSE +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/README.md +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/setup.cfg +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_asks.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_attachments.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_auto.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_cli.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_codetools.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_confidential.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_dirs.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_folders.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_images.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_outputs.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_plugins.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_policy.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_ports.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_registry.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_setup.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_ui.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_uitools.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_winagent.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_winbg.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.6.10 → witbitz_code-1.7.0}/tests/test_wslstart.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.0"
|
|
8
8
|
description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -41,6 +41,8 @@ from .plugins import notes_plugin_path as default_notes_plugin, notes_root as de
|
|
|
41
41
|
from .payers import SessionPayers
|
|
42
42
|
from .seen import SEEN_ROUTE, merge_seen, norm_seen
|
|
43
43
|
from .dirs import DIRS_ROUTE, list_dirs
|
|
44
|
+
from . import seccheck
|
|
45
|
+
from .seccheck import SECURITY_ROUTE
|
|
44
46
|
from .mkdir import MKDIR_ROUTE, mkdir_in
|
|
45
47
|
from .pairings import DEFAULT_OPENCODE_URL, hostname, read_env_password
|
|
46
48
|
from .relay import RELAY_URL, Connect, RelayPeer, allowed_event_path, allowed_request, project_response
|
|
@@ -157,6 +159,7 @@ class PairingServer:
|
|
|
157
159
|
asks_reconcile_ms: float = ASKS_RECONCILE_MS, notes_root: str | None = None,
|
|
158
160
|
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None) -> None:
|
|
159
161
|
self.pairing = pairing
|
|
162
|
+
self._security: seccheck.SecurityCheck | None = None # read on the first ask (seccheck.py)
|
|
160
163
|
# WHO PAYS (payers.py): this pairing's account owns the turns it starts; the proxy bills each call to its owner.
|
|
161
164
|
self._payers = payers
|
|
162
165
|
self._cid = _js.js_string(pairing.get("computerId") or "")
|
|
@@ -407,6 +410,14 @@ class PairingServer:
|
|
|
407
410
|
st, b = list_dirs(path=q.get("path", ""))
|
|
408
411
|
await reply(st, b)
|
|
409
412
|
return
|
|
413
|
+
# "THIS COMPUTER'S SECURITY" (seccheck.py): the same check as the witbitz-code page, for the account's own Spaces app —
|
|
414
|
+
# read here, sealed through the relay like every answer, never written to the account's stores. Python only (the Node
|
|
415
|
+
# download answers 404 and the page says the check is not available there).
|
|
416
|
+
if bare == SECURITY_ROUTE and m.get("m") == "GET":
|
|
417
|
+
if self._security is None:
|
|
418
|
+
self._security = seccheck.SecurityCheck()
|
|
419
|
+
await reply(200, await asyncio.to_thread(self._security.status))
|
|
420
|
+
return
|
|
410
421
|
# "NEW FOLDER" (mkdir.py = tools/code-mkdir.mjs): one clean leaf under the folder the picker shows. Answered HERE.
|
|
411
422
|
if bare == MKDIR_ROUTE and m.get("m") == "POST":
|
|
412
423
|
q = dict(urllib.parse.parse_qsl(query, keep_blank_values=True))
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""THIS COMPUTER'S SECURITY CHECK (the owner, 2026-10-12: infostealers on a desktop — "can the helper check the desktop status
|
|
2
|
+
and recommend what to do" → "Yes"). witbitz-code runs as an ordinary program on the computer, so it can read what a web page
|
|
3
|
+
never can: Windows' antivirus, its settings, the firewall, disk encryption, updates, the account's rights. It READS without
|
|
4
|
+
an administrator; a Fix that changes a setting goes through Windows' own admin prompt (UAC), and nothing changes silently.
|
|
5
|
+
A setting Windows wants changed in its own app (memory integrity: driver checks; BitLocker: a recovery key) opens that page.
|
|
6
|
+
|
|
7
|
+
It is ADVICE, not proof: malware already running as this user can fake anything a program running as this user reads.
|
|
8
|
+
It finds weak settings before something gets in; it cannot say that nothing has. The page and Spaces say so.
|
|
9
|
+
|
|
10
|
+
Windows only for now (also from WSL, where it reads the Windows that hosts it). Other systems: {"supported": False}.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from typing import Callable
|
|
23
|
+
|
|
24
|
+
SECURITY_ROUTE = "/witbitz/security" # the connector answers it for the paired Spaces app (connector.py)
|
|
25
|
+
|
|
26
|
+
# ONE PowerShell read. Every part is guarded: a part Windows refuses reads as null, never as a failure of the whole check.
|
|
27
|
+
PROBE_PS = r"""
|
|
28
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
29
|
+
$o = [ordered]@{}
|
|
30
|
+
$s = Get-MpComputerStatus
|
|
31
|
+
if ($s) { $o.defender = [ordered]@{ realtime = [bool]$s.RealTimeProtectionEnabled; behavior = [bool]$s.BehaviorMonitorEnabled;
|
|
32
|
+
tamper = [bool]$s.IsTamperProtected; sigAgeDays = [int]$s.AntivirusSignatureAge; mode = "$($s.AMRunningMode)" } }
|
|
33
|
+
$p = Get-MpPreference
|
|
34
|
+
if ($p) { $o.prefs = [ordered]@{ cloud = [int]$p.MAPSReporting; pua = [int]$p.PUAProtection; network = [int]$p.EnableNetworkProtection } }
|
|
35
|
+
$o.antivirus = @(Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct | ForEach-Object {
|
|
36
|
+
[ordered]@{ name = "$($_.displayName)"; on = ((($_.productState -shr 12) -band 0xF) -eq 1) } })
|
|
37
|
+
$dg = Get-CimInstance -Namespace root/Microsoft/Windows/DeviceGuard -ClassName Win32_DeviceGuard
|
|
38
|
+
if ($dg) { $o.memoryIntegrity = [bool](@($dg.SecurityServicesRunning) -contains 2) }
|
|
39
|
+
$o.firewall = @(Get-NetFirewallProfile | ForEach-Object { [ordered]@{ name = "$($_.Name)"; on = [bool]$_.Enabled } })
|
|
40
|
+
try { $bl = (New-Object -ComObject Shell.Application).NameSpace("$env:SystemDrive\").Self.ExtendedProperty('System.Volume.BitLockerProtection'); if ($bl -ne $null) { $o.bitlocker = [int]$bl } } catch {}
|
|
41
|
+
try { $d = (New-Object -ComObject Microsoft.Update.AutoUpdate).Results.LastInstallationSuccessDate; if ($d) { $o.updateDaysAgo = [int]((Get-Date) - [datetime]$d).TotalDays } } catch {}
|
|
42
|
+
$uac = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
|
43
|
+
if ($uac) { $o.uac = [ordered]@{ on = ([int]$uac.EnableLUA -eq 1); prompt = [int]$uac.ConsentPromptBehaviorAdmin } }
|
|
44
|
+
$o.admin = [bool]((whoami /groups /fo csv | Out-String) -match 'S-1-5-32-544')
|
|
45
|
+
$o.os = "$((Get-CimInstance Win32_OperatingSystem).Caption)"
|
|
46
|
+
$o | ConvertTo-Json -Depth 4 -Compress
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# The fixes. "ps" runs ELEVATED through Windows' admin prompt; "open" opens Windows' own page for a setting it checks itself.
|
|
50
|
+
FIXES = {
|
|
51
|
+
"pua": {"ps": "Set-MpPreference -PUAProtection Enabled"},
|
|
52
|
+
"network": {"ps": "Set-MpPreference -EnableNetworkProtection Enabled"},
|
|
53
|
+
"cloud": {"ps": "Set-MpPreference -MAPSReporting Advanced"},
|
|
54
|
+
"firewall": {"ps": "Set-NetFirewallProfile -All -Enabled True"},
|
|
55
|
+
"signatures": {"ps": "Update-MpSignature"},
|
|
56
|
+
"antivirus": {"open": "windowsdefender://threat"},
|
|
57
|
+
"memoryIntegrity": {"open": "windowsdefender://coreisolation"},
|
|
58
|
+
"tamper": {"open": "windowsdefender://threatsettings"},
|
|
59
|
+
"realtime": {"open": "windowsdefender://threatsettings"},
|
|
60
|
+
"bitlocker": {"open": "ms-settings:deviceencryption"},
|
|
61
|
+
"updates": {"open": "ms-settings:windowsupdate"},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def recommendations(s: dict | None) -> list[dict]:
|
|
66
|
+
"""[{id, ok, title, why, fix?}] — what this computer has and what it should change, plainly. Pure: tests feed it readings."""
|
|
67
|
+
if not s:
|
|
68
|
+
return []
|
|
69
|
+
out: list[dict] = []
|
|
70
|
+
|
|
71
|
+
def add(id_: str, ok: bool | None, title: str, why: str = "") -> None:
|
|
72
|
+
if ok is None:
|
|
73
|
+
return # Windows did not say: say nothing rather than guess
|
|
74
|
+
item = {"id": id_, "ok": bool(ok), "title": title, "why": "" if ok else why}
|
|
75
|
+
if not ok and id_ in FIXES:
|
|
76
|
+
item["fix"] = "admin" if "ps" in FIXES[id_] else "open"
|
|
77
|
+
out.append(item)
|
|
78
|
+
|
|
79
|
+
other_av = [a for a in (s.get("antivirus") or []) if a.get("on") and "defender" not in (a.get("name") or "").lower()]
|
|
80
|
+
d = s.get("defender")
|
|
81
|
+
if other_av:
|
|
82
|
+
out.append({"id": "antivirus", "ok": True, "title": f"Antivirus: {other_av[0]['name']}", "why": ""})
|
|
83
|
+
elif d:
|
|
84
|
+
add("realtime", d.get("realtime") and d.get("behavior"), "Microsoft Defender real-time protection",
|
|
85
|
+
"Real-time protection is off — malware can run without being stopped. Turn it on in Windows Security.")
|
|
86
|
+
add("tamper", d.get("tamper"), "Tamper protection",
|
|
87
|
+
"Tamper protection is off — malware can turn Defender off before it acts.")
|
|
88
|
+
age = d.get("sigAgeDays")
|
|
89
|
+
add("signatures", None if age is None else age <= 3, "Virus definitions",
|
|
90
|
+
f"Defender's definitions are {age} days old — it will miss newer malware.")
|
|
91
|
+
p = s.get("prefs") or {}
|
|
92
|
+
add("cloud", None if p.get("cloud") is None else p["cloud"] >= 1, "Cloud protection",
|
|
93
|
+
"Cloud protection is off — brand-new malware is recognised much later.")
|
|
94
|
+
add("pua", None if p.get("pua") is None else p["pua"] == 1, "Blocking unwanted apps",
|
|
95
|
+
"Bundled and unwanted apps are only logged, not blocked. Stealers often arrive inside them.")
|
|
96
|
+
add("network", None if p.get("network") is None else p["network"] == 1, "Blocking malicious sites in every app",
|
|
97
|
+
"Network protection is off — only Edge blocks known malicious sites; other apps and malware can still reach them.")
|
|
98
|
+
else:
|
|
99
|
+
out.append({"id": "antivirus", "ok": False, "title": "Antivirus", "why": "No antivirus answered. Open Windows Security and turn Microsoft Defender on.",
|
|
100
|
+
"fix": "open"})
|
|
101
|
+
add("memoryIntegrity", s.get("memoryIntegrity"), "Memory integrity (Core isolation)",
|
|
102
|
+
"Memory integrity is off — malware that reaches the system's core is much harder to stop. Turning it on needs a restart.")
|
|
103
|
+
fw = s.get("firewall") or []
|
|
104
|
+
add("firewall", None if not fw else all(f.get("on") for f in fw), "Firewall",
|
|
105
|
+
"The firewall is off for: " + ", ".join(f["name"] for f in fw if not f.get("on")) + ".")
|
|
106
|
+
bl = s.get("bitlocker")
|
|
107
|
+
add("bitlocker", None if bl is None else bl in (1, 3, 5), "Disk encryption (BitLocker)",
|
|
108
|
+
"The disk is not encrypted — anyone with the laptop can read it. Turn on device encryption and keep its recovery key safe.")
|
|
109
|
+
days = s.get("updateDaysAgo")
|
|
110
|
+
add("updates", None if days is None else days <= 21, "Windows updates",
|
|
111
|
+
f"Windows last installed updates {days} days ago. Updates close the holes malware uses.")
|
|
112
|
+
uac = s.get("uac") or {}
|
|
113
|
+
add("uac", None if not uac else (uac.get("on") and uac.get("prompt") != 0), "Admin prompt (UAC)",
|
|
114
|
+
"UAC is off or never asks — any program you run gets full control of the computer.")
|
|
115
|
+
if s.get("admin"):
|
|
116
|
+
out.append({"id": "adminAccount", "ok": False, "title": "Daily account",
|
|
117
|
+
"why": "You use an administrator account every day. A standard account for daily use, with the admin account kept for installs, "
|
|
118
|
+
"stops much malware from taking over the whole computer. (Optional, but the strongest single step.)"})
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def summary(items: list[dict]) -> dict:
|
|
123
|
+
todo = [i for i in items if not i["ok"]]
|
|
124
|
+
return {"checked": len(items), "todo": len(todo), "titles": [i["title"] for i in todo]}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── running it ──────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
def _in_wsl() -> bool:
|
|
129
|
+
try:
|
|
130
|
+
with open("/proc/version", encoding="utf-8") as f:
|
|
131
|
+
return "microsoft" in f.read().lower()
|
|
132
|
+
except OSError:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def powershell() -> str | None:
|
|
137
|
+
"""The PowerShell that reads Windows: native, or the host's from WSL. None off Windows."""
|
|
138
|
+
if sys.platform == "win32":
|
|
139
|
+
return shutil.which("powershell.exe") or shutil.which("powershell") or os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
140
|
+
if _in_wsl():
|
|
141
|
+
return shutil.which("powershell.exe") or ("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" if os.path.exists("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe") else None)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _run_ps(ps: str, script: str, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
146
|
+
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
147
|
+
return subprocess.run([ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], # noqa: S603
|
|
148
|
+
check=False, stdin=subprocess.DEVNULL, capture_output=True, text=True, errors="replace",
|
|
149
|
+
timeout=timeout, creationflags=flags, cwd="/mnt/c" if _in_wsl() and os.path.isdir("/mnt/c") else None)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_status(run: Callable[[str, str], subprocess.CompletedProcess] = _run_ps, ps: str | None = None) -> dict:
|
|
153
|
+
ps = ps or powershell()
|
|
154
|
+
if not ps:
|
|
155
|
+
return {"supported": False}
|
|
156
|
+
r = run(ps, PROBE_PS)
|
|
157
|
+
out = (r.stdout or "").strip().splitlines()
|
|
158
|
+
try:
|
|
159
|
+
return {"supported": True, **json.loads(out[-1] if out else "")}
|
|
160
|
+
except (ValueError, IndexError):
|
|
161
|
+
return {"supported": True, "error": "Windows did not answer the check" + (f" ({(r.stderr or '').strip()[:120]})" if r.stderr else "")}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def fix(item_id: str, run: Callable[[str, str], subprocess.CompletedProcess] = _run_ps, ps: str | None = None) -> dict:
|
|
165
|
+
"""Apply one fix: an admin change through Windows' UAC prompt, or Windows' own page for it. → {ok, why?}"""
|
|
166
|
+
f = FIXES.get(item_id)
|
|
167
|
+
ps = ps or powershell()
|
|
168
|
+
if not f or not ps:
|
|
169
|
+
return {"ok": False, "why": "nothing here can change that"}
|
|
170
|
+
if "open" in f:
|
|
171
|
+
r = run(ps, f"Start-Process '{f['open']}'", 30)
|
|
172
|
+
return {"ok": r.returncode == 0, "opened": True, **({} if r.returncode == 0 else {"why": "Windows did not open the page"})}
|
|
173
|
+
# Elevated: Windows shows its admin prompt; -Wait so the check that follows reads the result. A No at the prompt fails Start-Process.
|
|
174
|
+
inner = f["ps"].replace("'", "''")
|
|
175
|
+
script = (f"try {{ $p = Start-Process powershell -Verb RunAs -Wait -PassThru -WindowStyle Hidden "
|
|
176
|
+
f"-ArgumentList '-NoProfile','-Command','{inner}'; exit $p.ExitCode }} catch {{ exit 1223 }}")
|
|
177
|
+
r = run(ps, script, 300)
|
|
178
|
+
if r.returncode == 1223:
|
|
179
|
+
return {"ok": False, "why": "the admin prompt was declined — nothing changed"}
|
|
180
|
+
return {"ok": r.returncode == 0, **({} if r.returncode == 0 else {"why": f"Windows refused it (code {r.returncode})"})}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SecurityCheck:
|
|
184
|
+
"""{supported, items, summary, at, os, error?} for the page; read at most once a minute, again right after a fix."""
|
|
185
|
+
|
|
186
|
+
TTL = 60
|
|
187
|
+
|
|
188
|
+
def __init__(self, *, read: Callable[[], dict] | None = None, apply: Callable[[str], dict] | None = None,
|
|
189
|
+
now: Callable[[], float] = time.monotonic) -> None:
|
|
190
|
+
self._read = read or (lambda: read_status()) # looked up at call time
|
|
191
|
+
self._apply = apply or (lambda i: fix(i))
|
|
192
|
+
self._now = now
|
|
193
|
+
self._cache: tuple[float, dict] | None = None
|
|
194
|
+
self._task: asyncio.Task | None = None
|
|
195
|
+
|
|
196
|
+
def status(self, fresh: bool = False) -> dict:
|
|
197
|
+
if not fresh and self._cache and self._now() - self._cache[0] < self.TTL:
|
|
198
|
+
return self._cache[1]
|
|
199
|
+
try:
|
|
200
|
+
raw = self._read()
|
|
201
|
+
except Exception as e: # noqa: BLE001 — a reading, never a failure of the page
|
|
202
|
+
raw = {"supported": True, "error": f"could not check ({str(e)[:80]})"}
|
|
203
|
+
if not raw.get("supported"):
|
|
204
|
+
out = {"supported": False}
|
|
205
|
+
else:
|
|
206
|
+
items = recommendations(raw if not raw.get("error") else None)
|
|
207
|
+
out = {"supported": True, "items": items, "summary": summary(items), "os": raw.get("os") or "",
|
|
208
|
+
"at": int(time.time()), **({"error": raw["error"]} if raw.get("error") else {})}
|
|
209
|
+
self._cache = (self._now(), out)
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
def peek(self) -> dict:
|
|
213
|
+
"""What the page shows NOW, never waiting on Windows (a read takes seconds): the last reading, and a fresh one started
|
|
214
|
+
in the background when it is stale. {"checking": True} before the first one lands."""
|
|
215
|
+
stale = not self._cache or self._now() - self._cache[0] >= self.TTL
|
|
216
|
+
if stale and not (self._task and not self._task.done()):
|
|
217
|
+
try:
|
|
218
|
+
self._task = asyncio.get_running_loop().create_task(asyncio.to_thread(self.status, True))
|
|
219
|
+
except RuntimeError:
|
|
220
|
+
pass
|
|
221
|
+
return self._cache[1] if self._cache else {"checking": True}
|
|
222
|
+
|
|
223
|
+
async def fix(self, item_id: str) -> dict:
|
|
224
|
+
r = await asyncio.to_thread(self._apply, item_id)
|
|
225
|
+
await asyncio.to_thread(self.status, True) # the card shows the result at once
|
|
226
|
+
return r
|
|
@@ -35,7 +35,7 @@ from importlib import resources
|
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
from typing import Any, Awaitable, Callable
|
|
37
37
|
|
|
38
|
-
from . import __version__, devicelink, link, net, ports, qr, uitools, uninstall, winbg, wizard, wslstart
|
|
38
|
+
from . import __version__, devicelink, link, net, ports, qr, seccheck, uitools, uninstall, winbg, wizard, wslstart
|
|
39
39
|
from . import pair as pair_cmd
|
|
40
40
|
from . import pairings as pp
|
|
41
41
|
|
|
@@ -127,10 +127,12 @@ class Manager:
|
|
|
127
127
|
login_enable: Callable[[dict], tuple] = wslstart.startup_enable,
|
|
128
128
|
login_disable: Callable[[dict], bool] = wslstart.startup_disable,
|
|
129
129
|
port_owners: Callable[[int], list] | None = None, stop_pid: Callable[[int], bool] | None = None,
|
|
130
|
-
tools: uitools.Tools | None = None, label: dict | None = None, log: deque | None = None
|
|
130
|
+
tools: uitools.Tools | None = None, label: dict | None = None, log: deque | None = None,
|
|
131
|
+
security: seccheck.SecurityCheck | None = None) -> None:
|
|
131
132
|
self._serve, self._is_listening, self._pair, self._link = serve, is_listening, pair, link_node
|
|
132
133
|
self._load, self._keys_line = load_pairings, keys_line
|
|
133
134
|
self.tools = tools or uitools.Tools()
|
|
135
|
+
self.security = security or seccheck.SecurityCheck()
|
|
134
136
|
self._owners = port_owners or ports.port_owners
|
|
135
137
|
self._stop_pid = stop_pid or uninstall.stop_process
|
|
136
138
|
self._elsewhere_cache: tuple[float, dict | None] | None = None
|
|
@@ -214,7 +216,7 @@ class Manager:
|
|
|
214
216
|
serving = {"state": "elsewhere", "port": el["port"], "owners": el["owners"]}
|
|
215
217
|
return {"version": __version__, "computer": self.label, "paired": paired, "serving": serving, "pair": pair,
|
|
216
218
|
"login": self.login(), "busy": self.busy, "background": self.background,
|
|
217
|
-
"tools": await asyncio.to_thread(self.tools.status), "log": list(self.log)[-14:]}
|
|
219
|
+
"tools": await asyncio.to_thread(self.tools.status), "security": self.security.peek(), "log": list(self.log)[-14:]}
|
|
218
220
|
|
|
219
221
|
async def elsewhere(self, fresh: bool = False) -> dict | None:
|
|
220
222
|
"""{port, owners} when THIS person's other process holds OpenCode's port or the model proxy's (port + 100) while this
|
|
@@ -535,6 +537,7 @@ class Server:
|
|
|
535
537
|
"/api/login": lambda: self.m.set_login(bool(data.get("on"))),
|
|
536
538
|
"/api/quit": self.m.quit,
|
|
537
539
|
"/api/tools/install": self.m.tools.install,
|
|
540
|
+
"/api/security/fix": lambda: self.m.security.fix(str(data.get("id") or "")),
|
|
538
541
|
}
|
|
539
542
|
act = actions.get(path)
|
|
540
543
|
if not act:
|
|
@@ -51,3 +51,11 @@ footer { font-size: 13px; text-align: center; }
|
|
|
51
51
|
.actions:not(:has(button:not([hidden]))) { display: none; }
|
|
52
52
|
button.small { font-size: 13px; padding: 2px 10px; margin-left: 8px; }
|
|
53
53
|
pre.cmd { background: var(--off-bg); color: var(--text); padding: 8px 10px; border-radius: 8px; }
|
|
54
|
+
/* the security check (seccheck.py) */
|
|
55
|
+
p.small, div.small { font-size: 12.5px; }
|
|
56
|
+
.sec-list { list-style: none; padding: 0; margin: 8px 0; }
|
|
57
|
+
.sec-list li { padding: 8px 0; border-top: 1px solid var(--line, #e5e7eb); }
|
|
58
|
+
.sec-list li:first-child { border-top: 0; }
|
|
59
|
+
.sec-list li.todo .sec-head span { font-weight: 600; }
|
|
60
|
+
.sec-list li.ok { opacity: .7; }
|
|
61
|
+
.sec-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
@@ -97,6 +97,7 @@ function render(s) {
|
|
|
97
97
|
$('footerText').textContent = s.background ? 'runs in the background — Start menu → Witbitz Code opens this page' : 'keep its window open — closing it stops witbitz-code'
|
|
98
98
|
show($('quit'), !!s.background)
|
|
99
99
|
renderTools(s.tools, paired)
|
|
100
|
+
renderSecurity(s.security)
|
|
100
101
|
|
|
101
102
|
$('log').textContent = (s.log || []).join('\n')
|
|
102
103
|
}
|
|
@@ -161,6 +162,46 @@ function renderTools(t, paired) {
|
|
|
161
162
|
$('toolsLog').textContent = lines.join('\n')
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// This computer's security check (seccheck.py): what Windows has, what to change, and a Fix for what can be fixed from here.
|
|
166
|
+
function renderSecurity(sec) {
|
|
167
|
+
const card = $('secCard')
|
|
168
|
+
if (!sec || sec.supported === false) return show(card, false)
|
|
169
|
+
show(card, true)
|
|
170
|
+
const items = sec.items || []
|
|
171
|
+
const todo = items.filter((i) => !i.ok)
|
|
172
|
+
$('secText').textContent = sec.checking ? 'Checking Windows…'
|
|
173
|
+
: sec.error ? sec.error
|
|
174
|
+
: todo.length ? `${todo.length} ${todo.length === 1 ? 'thing' : 'things'} to change` + (sec.os ? ` · ${sec.os}` : '')
|
|
175
|
+
: 'Everything checked here is set well ✓' + (sec.os ? ` · ${sec.os}` : '')
|
|
176
|
+
const list = $('secList')
|
|
177
|
+
const key = JSON.stringify(items) + '|' + !!(state && state.busy)
|
|
178
|
+
if (list.dataset.key === key) return
|
|
179
|
+
list.dataset.key = key
|
|
180
|
+
list.textContent = ''
|
|
181
|
+
for (const i of [...todo, ...items.filter((x) => x.ok)]) {
|
|
182
|
+
const li = document.createElement('li')
|
|
183
|
+
li.className = i.ok ? 'ok' : 'todo'
|
|
184
|
+
const head = document.createElement('div')
|
|
185
|
+
head.className = 'sec-head'
|
|
186
|
+
const t = document.createElement('span')
|
|
187
|
+
t.textContent = (i.ok ? '✓ ' : '! ') + i.title
|
|
188
|
+
head.append(t)
|
|
189
|
+
if (i.fix) {
|
|
190
|
+
const b = document.createElement('button')
|
|
191
|
+
b.type = 'button'
|
|
192
|
+
b.className = 'small'
|
|
193
|
+
b.textContent = i.fix === 'admin' ? 'Fix' : 'Open in Windows'
|
|
194
|
+
b.disabled = !!(state && state.busy)
|
|
195
|
+
b.onclick = () => act('/api/security/fix', { id: i.id },
|
|
196
|
+
i.fix === 'admin' ? `Change it now? Windows will ask for permission (an admin prompt) — say Yes there.` : '')
|
|
197
|
+
head.append(b)
|
|
198
|
+
}
|
|
199
|
+
li.append(head)
|
|
200
|
+
if (i.why) { const w = document.createElement('div'); w.className = 'muted small'; w.textContent = i.why; li.append(w) }
|
|
201
|
+
list.append(li)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
164
205
|
let quitting = false
|
|
165
206
|
async function quit() {
|
|
166
207
|
if (!window.confirm('Quit Witbitz Code? Code in Spaces will not reach this computer until you open Witbitz Code from the Start menu again.')) return
|
|
@@ -51,6 +51,14 @@
|
|
|
51
51
|
<pre id="toolsLog" class="cmd" hidden></pre>
|
|
52
52
|
</section>
|
|
53
53
|
|
|
54
|
+
<section class="card" id="secCard" hidden>
|
|
55
|
+
<h2>This computer's security</h2>
|
|
56
|
+
<p id="secText" class="muted"></p>
|
|
57
|
+
<ul id="secList" class="sec-list"></ul>
|
|
58
|
+
<p class="muted small">A check of Windows' settings, not a malware scan. It finds weak settings before something gets in;
|
|
59
|
+
malware already running here could hide from it.</p>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
54
62
|
<section class="card" id="loginCard" hidden>
|
|
55
63
|
<label class="switch-row" for="login">
|
|
56
64
|
<span>
|
|
@@ -38,6 +38,7 @@ src/witbitz_code/ports.py
|
|
|
38
38
|
src/witbitz_code/qr.py
|
|
39
39
|
src/witbitz_code/receipt.py
|
|
40
40
|
src/witbitz_code/relay.py
|
|
41
|
+
src/witbitz_code/seccheck.py
|
|
41
42
|
src/witbitz_code/seen.py
|
|
42
43
|
src/witbitz_code/tinfoil_read.py
|
|
43
44
|
src/witbitz_code/tools_catalog.json
|
|
@@ -87,6 +88,7 @@ tests/test_ports.py
|
|
|
87
88
|
tests/test_ports_win.py
|
|
88
89
|
tests/test_registry.py
|
|
89
90
|
tests/test_relay_codec.py
|
|
91
|
+
tests/test_seccheck.py
|
|
90
92
|
tests/test_setup.py
|
|
91
93
|
tests/test_tools_probe.py
|
|
92
94
|
tests/test_toolsetup.py
|
|
@@ -893,3 +893,26 @@ def test_a_second_account_cannot_steer_into_a_turn_the_first_one_is_running():
|
|
|
893
893
|
await b.peer.aclose()
|
|
894
894
|
|
|
895
895
|
run(go())
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def test_the_python_connector_answers_this_computers_security_check(monkeypatch):
|
|
899
|
+
"""GET /witbitz/security (seccheck.py), sealed through the relay like any answer; OpenCode never hears of it."""
|
|
900
|
+
from witbitz_code import seccheck
|
|
901
|
+
from test_seccheck import OWNER
|
|
902
|
+
monkeypatch.setattr(seccheck, "read_status", lambda *a, **k: OWNER)
|
|
903
|
+
|
|
904
|
+
async def go():
|
|
905
|
+
async with Rig() as rig:
|
|
906
|
+
connector = PyConnector(rig, 2500, attachRoot=str(rig.attach_root))
|
|
907
|
+
await connector.start()
|
|
908
|
+
client = PyClient(rig.secret, rig.relay.url)
|
|
909
|
+
try:
|
|
910
|
+
await client.start()
|
|
911
|
+
r = await client.call("GET", "/witbitz/security")
|
|
912
|
+
body = json.loads(r["b"])
|
|
913
|
+
assert r["st"] == 200 and body["supported"] is True and body["summary"]["todo"] == 4, body
|
|
914
|
+
assert not any(h.get("url", "").startswith("/witbitz") for h in rig.oc.seen_copy())
|
|
915
|
+
finally:
|
|
916
|
+
await client.peer.aclose()
|
|
917
|
+
await connector.stop()
|
|
918
|
+
run(go())
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""This computer's security check (seccheck.py): Windows' readings → plain recommendations; a Fix goes through Windows' admin
|
|
2
|
+
prompt or opens Windows' own page; the page never waits on Windows."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from witbitz_code import seccheck
|
|
11
|
+
from test_ui import World, with_server, authed
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def until(fn, s=5.0):
|
|
15
|
+
for _ in range(int(s / 0.05)):
|
|
16
|
+
if fn():
|
|
17
|
+
return True
|
|
18
|
+
await asyncio.sleep(0.05)
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
# The owner's laptop, as read on 2026-10-12 (Windows 11 Pro).
|
|
22
|
+
OWNER = {"supported": True, "defender": {"realtime": True, "behavior": True, "tamper": True, "sigAgeDays": 1, "mode": "Normal"},
|
|
23
|
+
"prefs": {"cloud": 2, "pua": 2, "network": 0}, "antivirus": [{"name": "Windows Defender", "on": True}],
|
|
24
|
+
"memoryIntegrity": False, "firewall": [{"name": "Domain", "on": True}, {"name": "Private", "on": True}, {"name": "Public", "on": True}],
|
|
25
|
+
"bitlocker": 1, "updateDaysAgo": 1, "uac": {"on": True, "prompt": 5}, "admin": True, "os": "Microsoft Windows 11 Pro"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def by_id(items):
|
|
29
|
+
return {i["id"]: i for i in items}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_the_owners_laptop_reads_as_the_four_things_to_change():
|
|
33
|
+
items = by_id(seccheck.recommendations(OWNER))
|
|
34
|
+
todo = sorted(k for k, v in items.items() if not v["ok"])
|
|
35
|
+
assert todo == ["adminAccount", "memoryIntegrity", "network", "pua"]
|
|
36
|
+
assert items["pua"]["fix"] == "admin" and items["network"]["fix"] == "admin"
|
|
37
|
+
assert items["memoryIntegrity"]["fix"] == "open", "Windows checks drivers first — its own switch, not a registry write"
|
|
38
|
+
assert "fix" not in items["adminAccount"], "a separate account is advice, never something we change"
|
|
39
|
+
for k in ("realtime", "tamper", "signatures", "cloud", "firewall", "bitlocker", "updates", "uac"):
|
|
40
|
+
assert items[k]["ok"] and not items[k]["why"], k
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_another_antivirus_replaces_defenders_rows():
|
|
44
|
+
s = {**OWNER, "antivirus": [{"name": "Windows Defender", "on": False}, {"name": "ESET Security", "on": True}], "defender": {"realtime": False}}
|
|
45
|
+
items = by_id(seccheck.recommendations(s))
|
|
46
|
+
assert items["antivirus"] == {"id": "antivirus", "ok": True, "title": "Antivirus: ESET Security", "why": ""}
|
|
47
|
+
assert not {"realtime", "pua", "network", "cloud"} & set(items), "Defender is passive there — its settings are not the protection"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_no_antivirus_at_all_says_so_and_opens_windows_security():
|
|
51
|
+
items = by_id(seccheck.recommendations({**OWNER, "antivirus": [], "defender": None, "prefs": None}))
|
|
52
|
+
assert items["antivirus"]["ok"] is False and items["antivirus"]["fix"] == "open"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_what_windows_did_not_say_is_not_guessed():
|
|
56
|
+
items = by_id(seccheck.recommendations({"defender": {"realtime": True, "behavior": True, "tamper": True}}))
|
|
57
|
+
assert set(items) == {"realtime", "tamper"}, items
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_weak_readings_each_get_a_reason():
|
|
61
|
+
s = {**OWNER, "defender": {**OWNER["defender"], "tamper": False, "sigAgeDays": 9}, "firewall": [{"name": "Public", "on": False}],
|
|
62
|
+
"bitlocker": 2, "updateDaysAgo": 60, "uac": {"on": True, "prompt": 0}, "admin": False}
|
|
63
|
+
items = by_id(seccheck.recommendations(s))
|
|
64
|
+
assert "9 days" in items["signatures"]["why"] and "Public" in items["firewall"]["why"] and "60 days" in items["updates"]["why"]
|
|
65
|
+
assert not items["bitlocker"]["ok"] and not items["uac"]["ok"] and "adminAccount" not in items
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ran(code=0, out=""):
|
|
69
|
+
calls = []
|
|
70
|
+
|
|
71
|
+
def run(ps, script, timeout=60):
|
|
72
|
+
calls.append(script)
|
|
73
|
+
return subprocess.CompletedProcess([ps], code, out, "")
|
|
74
|
+
return run, calls
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_an_admin_fix_runs_elevated_through_the_uac_prompt():
|
|
78
|
+
run, calls = ran(0)
|
|
79
|
+
assert seccheck.fix("pua", run=run, ps="powershell.exe") == {"ok": True}
|
|
80
|
+
assert "-Verb RunAs" in calls[0] and "Set-MpPreference -PUAProtection Enabled" in calls[0]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_a_declined_prompt_changes_nothing_and_says_so():
|
|
84
|
+
run, _ = ran(1223)
|
|
85
|
+
r = seccheck.fix("network", run=run, ps="powershell.exe")
|
|
86
|
+
assert r["ok"] is False and "declined" in r["why"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_an_open_fix_opens_windows_own_page_and_unknown_ids_are_refused():
|
|
90
|
+
run, calls = ran(0)
|
|
91
|
+
assert seccheck.fix("memoryIntegrity", run=run, ps="powershell.exe")["opened"] is True
|
|
92
|
+
assert "windowsdefender://coreisolation" in calls[0]
|
|
93
|
+
assert seccheck.fix("adminAccount", run=run, ps="powershell.exe")["ok"] is False
|
|
94
|
+
assert seccheck.fix("rm -rf", run=run, ps="powershell.exe")["ok"] is False and len(calls) == 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_the_probe_output_is_read_and_a_garbled_one_is_an_error():
|
|
98
|
+
run, _ = ran(0, "WARNING: noise\n" + json.dumps({k: v for k, v in OWNER.items() if k != "supported"}))
|
|
99
|
+
assert seccheck.read_status(run=run, ps="ps")["prefs"]["pua"] == 2
|
|
100
|
+
run, _ = ran(1, "not json")
|
|
101
|
+
assert "error" in seccheck.read_status(run=run, ps="ps")
|
|
102
|
+
assert seccheck.read_status() == {"supported": False}, "no PowerShell (macOS, Linux; and every test): not supported"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_the_page_never_waits_on_windows():
|
|
106
|
+
import time
|
|
107
|
+
|
|
108
|
+
def slow_read():
|
|
109
|
+
time.sleep(0.4)
|
|
110
|
+
return OWNER
|
|
111
|
+
|
|
112
|
+
async def main():
|
|
113
|
+
sc = seccheck.SecurityCheck(read=slow_read)
|
|
114
|
+
t0 = time.monotonic()
|
|
115
|
+
assert sc.peek() == {"checking": True} and time.monotonic() - t0 < 0.1
|
|
116
|
+
await until(lambda: "items" in sc.peek())
|
|
117
|
+
assert sc.peek()["summary"]["todo"] == 4
|
|
118
|
+
asyncio.run(main())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_the_page_shows_the_card_and_fixes_through_the_server():
|
|
122
|
+
applied = []
|
|
123
|
+
|
|
124
|
+
async def main():
|
|
125
|
+
w = World()
|
|
126
|
+
m = w.manager()
|
|
127
|
+
m.security = seccheck.SecurityCheck(read=lambda: OWNER if not applied else {**OWNER, "prefs": {**OWNER["prefs"], "pua": 1}},
|
|
128
|
+
apply=lambda i: applied.append(i) or {"ok": True})
|
|
129
|
+
srv, base = await with_server(m)
|
|
130
|
+
try:
|
|
131
|
+
async with httpx.AsyncClient() as c:
|
|
132
|
+
await until(lambda: "items" in m.security.peek())
|
|
133
|
+
s = (await c.get(base + "/api/status", headers=authed(srv))).json()["security"]
|
|
134
|
+
assert s["summary"]["todo"] == 4
|
|
135
|
+
r = await c.post(base + "/api/security/fix", headers=authed(srv, **{"content-type": "application/json"}), json={"id": "pua"})
|
|
136
|
+
assert r.json() == {"ok": True} and applied == ["pua"]
|
|
137
|
+
s = (await c.get(base + "/api/status", headers=authed(srv))).json()["security"]
|
|
138
|
+
assert s["summary"]["todo"] == 3, "read again right after the fix"
|
|
139
|
+
r = await c.post(base + "/api/security/fix", headers={"content-type": "application/json"}, json={"id": "pua"})
|
|
140
|
+
assert r.status_code in (401, 403), "only the page's own session may change Windows"
|
|
141
|
+
finally:
|
|
142
|
+
await srv.close()
|
|
143
|
+
asyncio.run(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
|
|
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
|
{witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.6.10 → witbitz_code-1.7.0}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
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
|
|
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
|
|
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
|
|
File without changes
|