Add agentless adoption tool for endpoints

Standalone one-shot enrollment tool that creates a ShopDB PC + Endpoint entry using only the endpoints collector key, reusing the existing collector adopt path. Collects identity, WMI hardware, and installed-software SBOM; requires admin. Server-side packaging route attaches to the existing endpoints blueprint; no core contract change.
This commit is contained in:
benjamin.baker
2026-08-30 08:31:31 -04:00
committed by cproudlock
parent cfb0b9ca4e
commit 7040832565
8 changed files with 685 additions and 0 deletions

View File

@@ -138,6 +138,57 @@
</div>
</section>
<!-- ====================================================================
Adoption tool (spike)
================================================================= -->
<section class="settings-section">
<h3>Adoption tool</h3>
<div class="card form-card">
<p class="field-hint">
Generate a one-shot, agentless enrollment tool. Run it once on a PC and
it creates a ShopDB PC and Endpoint entry using only the collector key -
no service, no polling, no ongoing telemetry. Use this for machines you
want inventoried without running the full endpoint agent.
</p>
<p class="field-hint">
Paste a collector key below (generate one above first; the secret is
only shown once). The key is embedded in the downloaded config and is
not stored on the server.
</p>
<label class="field-label" for="adopt-baseurl">Server base URL</label>
<input
id="adopt-baseurl"
v-model="adoptbaseurl"
class="form-control"
type="text"
placeholder="https://shopdb.example"
/>
<label class="field-label" for="adopt-key">Collector key</label>
<input
id="adopt-key"
v-model="adoptcollectorkey"
class="form-control mono"
type="text"
placeholder="Paste the endpoint agent key"
/>
<div class="button-row">
<button
class="btn btn-primary"
type="button"
:disabled="adoptbusy || !adoptbaseurl || !adoptcollectorkey"
@click="downloadAdoptionTool"
>
{{ adoptbusy ? 'Building...' : 'Download adoption tool' }}
</button>
</div>
<p v-if="adoptmessage" class="field-hint">{{ adoptmessage }}</p>
<p v-if="adopterror" class="field-error">{{ adopterror }}</p>
</div>
</section>
<!-- ====================================================================
Build site agent (placeholder)
================================================================= -->
@@ -197,6 +248,13 @@ const copied = ref(false)
const message = ref('')
const error = ref('')
// Adoption tool (spike): build a one-shot, agentless enrollment download.
const adoptbaseurl = ref('')
const adoptcollectorkey = ref('')
const adoptbusy = ref(false)
const adoptmessage = ref('')
const adopterror = ref('')
function formatDate(value) {
if (!value) return ''
const parsed = new Date(value)
@@ -209,6 +267,7 @@ function applyStatus(data) {
}
onMounted(async () => {
adoptbaseurl.value = window.location.origin
try {
const response = await api.get('/endpoints/agentkey')
applyStatus(response.data.data || {})
@@ -238,6 +297,9 @@ async function regenerate() {
const data = response.data.data || {}
applyStatus(data)
secret.value = data.secret || ''
if (data.secret) {
adoptcollectorkey.value = data.secret
}
message.value = 'Endpoint agent key generated.'
} catch (regenError) {
error.value = regenError.response?.data?.data?.error?.message
@@ -276,6 +338,34 @@ async function copySecret() {
console.error(copyError)
}
}
async function downloadAdoptionTool() {
adoptbusy.value = true
adoptmessage.value = ''
adopterror.value = ''
try {
const response = await api.post(
'/endpoints/adoptiontool/package',
{ baseurl: adoptbaseurl.value, collectorkey: adoptcollectorkey.value },
{ responseType: 'blob' })
const url = window.URL.createObjectURL(response.data)
const link = document.createElement('a')
link.href = url
link.download = 'shopdb-adoption-tool.zip'
document.body.appendChild(link)
link.click()
link.remove()
window.URL.revokeObjectURL(url)
adoptmessage.value = 'Adoption tool downloaded.'
} catch (downloadError) {
adopterror.value = downloadError.response?.status === 403
? 'Only an administrator can build the adoption tool'
: 'Could not build the adoption tool'
console.error(downloadError)
} finally {
adoptbusy.value = false
}
}
</script>
<style scoped>