Add curated manifest-entry -> Application link (honest app tracking)
All checks were successful
CI / backend (push) Successful in 1m34s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

The honest replacement for the backed-out auto-seeding: instead of scraping
manifest labels into duplicate Application rows, an entry can be LINKED to an
existing catalog Application, cross-referencing what shopdb already tracks.

- Model: manifestentries.appid (nullable soft ref to core applications; in the
  0001 baseline). It is shopdb METADATA, deliberately NOT a manifest field - it
  never appears in the rendered manifest JSON, so enforcement + parity are
  unaffected (test asserts it stays out of the preview manifest).
- API: _entry_payload returns appid + resolved appname; create/update accept an
  optional appid (validated, unknown id ignored, null unlinks) via _apply_app_link;
  GET /geenforce/applications is the picker source (id + name).
- Editor: a "Tracked application (optional)" select in the entry modal, and the
  entry summary line notes the linked app ("...; tracked: eDNC").
- Foundation for a future desired-vs-observed compliance view.

889 tests green (incl. the link test + parity/migration unaffected); build +
naming green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-13 06:45:10 -04:00
parent 810687953f
commit 3355436fcd
5 changed files with 100 additions and 5 deletions

View File

@@ -277,6 +277,16 @@
<small v-if="isPreinstallScope" class="input-hint">Preinstall runs at imaging and supports MSI/EXE only.</small>
</label>
</div>
<div class="setting-row">
<label>
<span>Tracked application (optional)</span>
<select v-model="entryForm.appid">
<option :value="null">(not linked)</option>
<option v-for="app in applications" :key="app.appid" :value="app.appid">{{ app.appname }}</option>
</select>
<small class="input-hint">Link this entry to a tracked Application in the catalog (metadata; not part of the manifest).</small>
</label>
</div>
</div>
<!-- payload fields per type -->
@@ -566,6 +576,7 @@ const newScope = ref({ scopename: '', phase: 'runtime' })
const showEntry = ref(false)
const entryForm = ref({})
const applications = ref([])
const showVersions = ref(false)
const versions = ref([])
@@ -645,7 +656,7 @@ async function deleteScope() {
// -- entries --
function blankEntry() {
return { Type: 'MSI', DetectionMethod: '', RegType: 'String',
inuseBehavior: '', inuseProcesses: [],
inuseBehavior: '', inuseProcesses: [], appid: null,
PreEnrollment: false, KillAfterDetection: false, PCTypesStrict: false }
}
function openNewEntry() {
@@ -682,7 +693,9 @@ function splitList(value) {
}
function buildEntryPayload() {
const form = entryForm.value
const out = { Name: form.Name, Type: form.Type }
// appid is shopdb metadata, always sent (null unlinks); the backend keeps it
// off the manifest JSON.
const out = { Name: form.Name, Type: form.Type, appid: form.appid ?? null }
const scalars = ['Installer', 'InstallArgs', 'Script', 'Args', 'Source',
'Destination', 'RegPath', 'RegName', 'RegType', 'DetectionPath',
'DetectionName', 'DetectionValue', 'DetectionPattern', '_CmmVersion',
@@ -828,14 +841,20 @@ function describeEntry(entry) {
} else if (entry.PCTypes && entry.PCTypes.length) {
text += `; ${entry.PCTypes.length} PC type(s)`
}
if (entry.appname) text += `; tracked: ${entry.appname}`
return text
}
function formatDate(value) {
return value ? new Date(value).toLocaleString() : ''
}
async function loadApplications() {
try { applications.value = payload(await api.get('/geenforce/applications')) } catch (e) { /* optional */ }
}
loadScopes()
loadConfig()
loadApplications()
</script>
<style scoped>