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:
@@ -1,9 +1,8 @@
|
||||
"""One-line bootstrap installer for the endpoint agent.
|
||||
|
||||
An admin mints a short-lived token in Settings > Endpoints and hands a tech one
|
||||
command. On a PC with no agent, in an ELEVATED PowerShell:
|
||||
|
||||
powershell -ExecutionPolicy Bypass -c "irm <base>/api/endpoints/siteagent/bootstrap.ps1?t=<token> | iex"
|
||||
command. On a PC with no agent, in an ELEVATED PowerShell: the command
|
||||
downloads bootstrap.ps1 to a file, runs it, and deletes it again.
|
||||
|
||||
That installs the agent and tray with NO shared key, so the PC asks to enroll
|
||||
and waits for an admin to approve it in Settings > Endpoints > Enrollment. This
|
||||
@@ -61,6 +60,13 @@ LIST_LIMIT = 100
|
||||
SCRIPT_PATH = '/api/endpoints/siteagent/bootstrap.ps1'
|
||||
PACKAGE_PATH = '/api/endpoints/siteagent/bootstrap/package'
|
||||
|
||||
# Where the one-line command drops the script before running it. A literal, not
|
||||
# $env:TEMP: the line has to survive being pasted into an already-running
|
||||
# PowerShell, which expands every $ inside the outer double quotes before the
|
||||
# child process sees them. The install needs elevation anyway, so this path is
|
||||
# writable by definition.
|
||||
COMMAND_SCRIPT_PATH = r'C:\Windows\Temp\shopdb-bootstrap.ps1'
|
||||
|
||||
# Fixed-window per-IP limiter, same shape and cache extension as the login
|
||||
# limiter in shopdb.core.api.auth and the GE-Enforce payload limiter. A tech
|
||||
# fetching a script and a package is two hits; a scanner walking token guesses
|
||||
@@ -145,11 +151,31 @@ def bootstrap_base_url():
|
||||
return 'https://{0}{1}'.format(host, root)
|
||||
|
||||
|
||||
def bootstrap_command(secret, baseurl=None):
|
||||
"""The one line a tech pastes into an elevated PowerShell."""
|
||||
def bootstrap_script_url(secret, baseurl=None):
|
||||
"""The token URL that serves the script: downloaded by the command, and
|
||||
offered in the UI as a plain file a tech can save and run instead."""
|
||||
base = (baseurl if baseurl is not None else bootstrap_base_url()).rstrip('/')
|
||||
return ('powershell -ExecutionPolicy Bypass -c "irm {0}{1}?t={2} | iex"'
|
||||
.format(base, SCRIPT_PATH, secret))
|
||||
return '{0}{1}?t={2}'.format(base, SCRIPT_PATH, secret)
|
||||
|
||||
|
||||
def bootstrap_command(secret, baseurl=None):
|
||||
"""The one line a tech pastes into an elevated PowerShell.
|
||||
|
||||
Download to a file and run it, never `irm ... | iex`: Microsoft Defender
|
||||
scores the piped-to-iex shape as Trojan:Win32/Commando.A!ml and kills the
|
||||
process before a line of it runs. Every bay runs Defender.
|
||||
|
||||
Single quotes inside and no $ anywhere, so the one line means the same
|
||||
whether it is pasted into cmd.exe or into a PowerShell prompt. The download
|
||||
and the run are wrapped in try/finally so the dropped file goes away even
|
||||
when the script exits non-zero or the download itself fails.
|
||||
"""
|
||||
return ('powershell -ExecutionPolicy Bypass -c "'
|
||||
"try {{ Invoke-WebRequest -UseBasicParsing '{0}' -OutFile '{1}'; "
|
||||
"& '{1}' }} finally {{ Remove-Item '{1}' -Force "
|
||||
'-ErrorAction SilentlyContinue }}"'
|
||||
).format(bootstrap_script_url(secret, baseurl),
|
||||
COMMAND_SCRIPT_PATH)
|
||||
|
||||
|
||||
def _hash_token(secret):
|
||||
@@ -448,6 +474,7 @@ def create_bootstrap_token():
|
||||
'token': row.to_dict(now),
|
||||
'secret': secret,
|
||||
'command': bootstrap_command(secret),
|
||||
'scripturl': bootstrap_script_url(secret),
|
||||
},
|
||||
message='Bootstrap token created. The command is shown once.',
|
||||
http_code=201)
|
||||
@@ -487,7 +514,10 @@ def revoke_bootstrap_token(tokenid):
|
||||
|
||||
@endpoints_bp.route('/siteagent/bootstrap.ps1', methods=['GET'])
|
||||
def serve_bootstrap_script():
|
||||
"""The PowerShell the one-line command pipes into iex.
|
||||
"""The PowerShell the one-line command downloads, runs and deletes.
|
||||
|
||||
The same URL is the UI's "Download .ps1" link, for a tech who would rather
|
||||
save the file and run it than paste a command.
|
||||
|
||||
Fetching the script does NOT spend a use: a tech who pastes the command
|
||||
twice, or a shell that retries, must not burn the budget the package
|
||||
|
||||
Reference in New Issue
Block a user