Make the one-line install survive Defender, and approval leave a PC that works
All three found testing the bootstrap installer on Windows. - The published command used the irm-pipe-iex form, which Microsoft Defender detects as Trojan:Win32/Commando.A!ml and kills before it runs, every time, on a default install. The command now downloads the script to a file, runs it, and removes it in a finally block, which Defender does not object to. The UI also offers the script as a plain download for a tech who would rather save it. - Approving a request only linked the asset, so a PC whose pending reason was "unknown hostname" - which is every PC installed this way - kept getting 403 on every call afterwards, because its computer record had no hostname to match the credential against. Approval now stamps the hostname when the record has none, refuses with a 409 naming both when the record has a different one, and overwrites only when the admin confirms. The Credentials tab offers that confirmation. - A hostname that resolves to no record now says so instead of reading as an impostor, while still refusing the request. - While waiting for approval the agent retried on the 15-minute telemetry cadence, so a PC approved immediately enrolled a quarter of an hour later and the installer always ended on "still pending". It now retries about every minute for the first ten minutes, then every five to thirty minutes, then every fifteen, with jitter. A rate limit or server error still uses the slower backoff.
This commit is contained in:
@@ -27,6 +27,17 @@ TOKENS_URL = '/api/endpoints/siteagent/bootstrap/tokens'
|
||||
SCRIPT_URL = '/api/endpoints/siteagent/bootstrap.ps1'
|
||||
PACKAGE_URL = '/api/endpoints/siteagent/bootstrap/package'
|
||||
BASE_URL = 'https://shopdb.test/shopdb'
|
||||
TEMP_SCRIPT = r'C:\Windows\Temp\shopdb-bootstrap.ps1'
|
||||
|
||||
|
||||
def _expected_command(secret, base=BASE_URL):
|
||||
"""Download, run, delete. Never `irm | iex` - Defender kills that shape."""
|
||||
return ('powershell -ExecutionPolicy Bypass -c "'
|
||||
"try {{ Invoke-WebRequest -UseBasicParsing "
|
||||
"'{0}/api/endpoints/siteagent/bootstrap.ps1?t={1}' "
|
||||
"-OutFile '{2}'; & '{2}' }} finally {{ "
|
||||
"Remove-Item '{2}' -Force -ErrorAction SilentlyContinue }}\""
|
||||
).format(base, secret, TEMP_SCRIPT)
|
||||
|
||||
|
||||
def _make_cert():
|
||||
@@ -98,10 +109,10 @@ def test_generate_returns_the_secret_and_the_ready_made_command(
|
||||
secret = data['secret']
|
||||
assert secret.startswith('sdbb_')
|
||||
assert len(secret) == len('sdbb_') + 43
|
||||
assert data['command'] == (
|
||||
'powershell -ExecutionPolicy Bypass -c "irm '
|
||||
'{0}/api/endpoints/siteagent/bootstrap.ps1?t={1} | iex"'.format(
|
||||
BASE_URL, secret))
|
||||
assert data['command'] == _expected_command(secret)
|
||||
assert data['scripturl'] == (
|
||||
'{0}/api/endpoints/siteagent/bootstrap.ps1?t={1}'.format(BASE_URL,
|
||||
secret))
|
||||
token = data['token']
|
||||
assert token['tokenprefix'] == secret[:12]
|
||||
assert token['maxuses'] == 3
|
||||
@@ -111,6 +122,29 @@ def test_generate_returns_the_secret_and_the_ready_made_command(
|
||||
assert token['createdby'] == 'testadmin'
|
||||
|
||||
|
||||
def test_the_command_downloads_and_runs_instead_of_piping_into_iex(
|
||||
client, db, auth_headers):
|
||||
"""Defender detects `irm ... | iex` as Trojan:Win32/Commando.A!ml and kills
|
||||
the process before a line of it runs, so the published command downloads to
|
||||
a file, runs it and removes it. It also stays one pasteable line, and holds
|
||||
no `$`: pasted into a PowerShell prompt, the parent would expand those
|
||||
before the child process ever saw them."""
|
||||
data = _secret(client, auth_headers)
|
||||
command = data['command']
|
||||
|
||||
assert ' | iex' not in command
|
||||
assert 'iex' not in command.lower()
|
||||
assert '$' not in command
|
||||
assert '\n' not in command
|
||||
assert data['scripturl'] in command
|
||||
assert data['secret'] in command
|
||||
assert "Remove-Item '{0}' -Force".format(TEMP_SCRIPT) in command
|
||||
assert 'finally' in command
|
||||
# The link the UI offers instead is the route that really serves it.
|
||||
assert client.get(SCRIPT_URL,
|
||||
query_string={'t': data['secret']}).status_code == 200
|
||||
|
||||
|
||||
def test_defaults_are_a_day_and_twenty_five_uses(client, db, auth_headers):
|
||||
token = _secret(client, auth_headers)['token']
|
||||
assert token['maxuses'] == 25
|
||||
@@ -383,6 +417,7 @@ def test_the_command_falls_back_to_the_request_when_no_site_url_is_set(
|
||||
client, db, auth_headers):
|
||||
Setting.set('site_base_url', '', valuetype='string')
|
||||
data = _secret(client, auth_headers)
|
||||
assert data['command'].startswith(
|
||||
'powershell -ExecutionPolicy Bypass -c "irm https://localhost'
|
||||
'/api/endpoints/siteagent/bootstrap.ps1?t=')
|
||||
assert data['command'] == _expected_command(data['secret'],
|
||||
base='https://localhost')
|
||||
assert data['scripturl'].startswith(
|
||||
'https://localhost/api/endpoints/siteagent/bootstrap.ps1?t=')
|
||||
|
||||
Reference in New Issue
Block a user