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:
@@ -65,6 +65,19 @@ REASON_NONCE_CHANGED = 'nonce changed'
|
||||
REASON_REJECTED = 'rejected'
|
||||
REASON_REENROLL_BLOCKED = 'reenroll blocked'
|
||||
|
||||
# What an approval did to the computer's hostname. Reported back so the UI can
|
||||
# say so, and so an admin can tell a rename from a no-op.
|
||||
HOSTNAME_UNCHANGED = 'unchanged'
|
||||
HOSTNAME_SET = 'set'
|
||||
HOSTNAME_OVERWRITTEN = 'overwritten'
|
||||
HOSTNAME_MESSAGES = {
|
||||
HOSTNAME_UNCHANGED: 'Enroll request approved',
|
||||
HOSTNAME_SET: 'Enroll request approved; the hostname is now recorded on '
|
||||
'that computer',
|
||||
HOSTNAME_OVERWRITTEN: 'Enroll request approved; that computer was renamed '
|
||||
'to the hostname the PC reports',
|
||||
}
|
||||
|
||||
MIGRATION_STATES = ('enrolled', 'pending', 'upgrading', 'ready', 'blocked',
|
||||
'notreporting')
|
||||
ENROLLMENT_STATES = ('enrolled', 'notenrolled', 'pending', 'revoked')
|
||||
@@ -557,7 +570,21 @@ def get_enrollment():
|
||||
methods=['POST'])
|
||||
@require_role('admin')
|
||||
def approve_enroll_request(requestid):
|
||||
"""Bind a pending request to the asset the admin picked."""
|
||||
"""Bind a pending request to the asset the admin picked.
|
||||
|
||||
The computer record also has to CARRY the request's hostname, or the PC
|
||||
403s on every call after it collects its credential: bind_hostname resolves
|
||||
the hostname the agent sends to an asset and compares it with the
|
||||
credential's asset, and a computer with no hostname resolves to nothing.
|
||||
The pending reason for a one-off PC is by definition `unknown hostname`, so
|
||||
this is the normal case, not the exception.
|
||||
|
||||
Body: {"assetid": 123, "sethostname": false}. A computer with no hostname
|
||||
is stamped with the request's. A computer carrying a DIFFERENT hostname is
|
||||
refused with 409 naming both, until the caller repeats the approval with
|
||||
"sethostname": true. Either change is audited, and the response says which
|
||||
of unchanged / set / overwritten happened.
|
||||
"""
|
||||
row = db.session.get(EndpointEnrollRequest, requestid)
|
||||
if row is None:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
@@ -572,8 +599,13 @@ def approve_enroll_request(requestid):
|
||||
if isinstance(assetid, bool) or not isinstance(assetid, int):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'assetid must be an integer')
|
||||
sethostname = payload.get('sethostname', False)
|
||||
if not isinstance(sethostname, bool):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'sethostname must be true or false')
|
||||
Computer = _computer_model()
|
||||
if Computer.query.filter_by(assetid=assetid).first() is None:
|
||||
computer = Computer.query.filter_by(assetid=assetid).first()
|
||||
if computer is None:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
f'Asset {assetid} is not a computer')
|
||||
if agentauth.active_credential_for(assetid) is not None:
|
||||
@@ -585,6 +617,35 @@ def approve_enroll_request(requestid):
|
||||
'That computer is blocked from enrolling again; '
|
||||
'allow re-enrollment first', http_code=409)
|
||||
|
||||
requesthostname = (row.hostname or '').strip()
|
||||
currenthostname = (computer.hostname or '').strip()
|
||||
hostnameresult = HOSTNAME_UNCHANGED
|
||||
if requesthostname and not currenthostname:
|
||||
hostnameresult = HOSTNAME_SET
|
||||
elif requesthostname and currenthostname.lower() != requesthostname.lower():
|
||||
if not sethostname:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
'That computer is recorded as {0}, but the request comes from '
|
||||
'{1}. Approving as it stands leaves the PC unable to '
|
||||
'authenticate. Approve again with "sethostname": true to '
|
||||
'rename the computer to {1}.'.format(currenthostname,
|
||||
requesthostname),
|
||||
details={'hostnameconflict': True,
|
||||
'computerhostname': currenthostname,
|
||||
'requesthostname': requesthostname},
|
||||
http_code=409)
|
||||
hostnameresult = HOSTNAME_OVERWRITTEN
|
||||
|
||||
if hostnameresult != HOSTNAME_UNCHANGED:
|
||||
computer.hostname = requesthostname
|
||||
AuditLog.log('updated', 'Computer', entityid=computer.computerid,
|
||||
entityname=requesthostname,
|
||||
changes={'hostname': {'old': currenthostname or None,
|
||||
'new': requesthostname}},
|
||||
details='hostname {0} from enroll request {1}'.format(
|
||||
hostnameresult, row.requestid))
|
||||
|
||||
row.status = EndpointEnrollRequest.STATUS_APPROVED
|
||||
row.matchedassetid = assetid
|
||||
row.reason = None
|
||||
@@ -592,9 +653,12 @@ def approve_enroll_request(requestid):
|
||||
row.decidedby = _username()
|
||||
AuditLog.log('updated', 'EndpointEnrollRequest', entityid=row.requestid,
|
||||
entityname=row.hostname,
|
||||
details='enroll request approved for asset {}'.format(assetid))
|
||||
details='enroll request approved for asset {0}, hostname {1}'
|
||||
.format(assetid, hostnameresult))
|
||||
db.session.commit()
|
||||
return success_response(row.to_dict(), message='Enroll request approved')
|
||||
approved = row.to_dict()
|
||||
approved['hostnameresult'] = hostnameresult
|
||||
return success_response(approved, message=HOSTNAME_MESSAGES[hostnameresult])
|
||||
|
||||
|
||||
@endpoints_bp.route('/enrollment/requests/<int:requestid>/reject',
|
||||
|
||||
Reference in New Issue
Block a user