Store manual profile names and freeze historical run labels

This commit is contained in:
omar
2026-09-23 17:57:09 +03:00
parent 823b048e7c
commit 529dcd4026
9 changed files with 90 additions and 64 deletions
+4
View File
@@ -5,3 +5,7 @@ This repository is authoritative only for its owned component. Use sibling clone
Edit here, prove the affected behavior, audit the explicit file allowlist, commit/push normally, then fetch reviewed full commits into the deployment host clones. Deploy through otche-deploy/update.py and its documented protected external env/override. No archive-based source deployment, force/reset, dirty-work overwrite or unreviewed latest-branch activation. Coordinate quiescence before activation; retain the exact existing Compose project, external named volumes, private secrets/TLS/config/proofs and held evidence. Verify live commit IDs, health, old-data continuity and requested acceptance after rollout.
Never commit real secrets, tokens, runtime output, sample payloads, proof logs, private certificates or live host inventory. No credentials in remote URLs or persistent helpers. Windows signing remains operator-supplied. Do not weaken VM/network/Windows protections to pass acceptance.
## Windows display names
The complete Windows display label, including build/version text, is explicitly authored in editable `Profile.name`. NEVER derive or append the display build from environment telemetry, qualification, a revision lookup or a frontend formatter. Each Run stores an immutable `profile_name` snapshot; profile renames do not rewrite historical names. Technical `environment.os_build` telemetry and source fingerprints remain separate and unchanged.
+3 -3
View File
@@ -16,7 +16,7 @@ Readable live logs are captured as bounded **best-effort open-length snapshots**
`GET /jobs?page=1&page_size=25&q=&status=` -> `{items:Job[],total,page,page_size,next_page:null|number}`; `GET /jobs/{id}` Job with runs; `POST /jobs/{id}/cancel` -> Job; `POST /jobs/{id}/retry` -> same Job with a new Attempt for each terminal Run, preserving every prior Attempt and original immutable settings/revisions, admission rechecked. Active Jobs cannot retry. Job `{id,owner_id,upload_id,filename,execution_filename,sha256,size,settings,status,created_at,updated_at,cancel_requested,runs:Run[]}`. Status `queued|running|completed|cancelled|failed`.
Run `{id,job_id,profile_id,profile_name,os_build:string|null,revision_id,antivirus:"defender",status,attempts:Attempt[]}`; status `queued|running|completed|cancelled|failed`. `os_build` belongs to the Run's exact revision, never the profile's later current revision.
Run `{id,job_id,profile_id,profile_name,revision_id,antivirus:"defender",status,attempts:Attempt[]}`; status `queued|running|completed|cancelled|failed`. `profile_name` is the fixed full name captured when that Run was created, including any manually entered build text; retries preserve it.
Attempt `{id,run_id,command_id,phase,outcome,findings,telemetry,cleanup,error,created_at,started_at,finished_at,deadline_at,allocation,report,artifacts:Artifact[]}`.
- phase: `queued|provisioning|booting|recording|delivering|preparing|running|collecting|stopping|cleanup|finished`.
@@ -98,9 +98,9 @@ Missing or partial telemetry is not an empty clean report. Fixed collector field
## Profiles and administration
`GET /profiles` -> list Profile `{id,name,os,os_build:string|null,architecture,enabled,qualification,state,current_revision_id,online_available,reason,metadata:object}`; state `maintenance|published`; qualification `unqualified|qualified|drifted`; metadata contains observed build/Defender baseline/policy only, no VM or token configuration to operators. Admission rejects unqualified/unconfigured profiles, not simulated runs.
`GET /profiles` -> list Profile `{id,name,os,architecture,enabled,qualification,state,current_revision_id,online_available,reason,metadata:object}`; state `maintenance|published`; qualification `unqualified|qualified|drifted`; metadata contains observed OS/Defender telemetry only, no VM/token secrets. `name` is the sole editable full display label. Admission rejects unqualified/unconfigured profiles.
Windows display labels preserve the user-defined base `name` and append the verified `os_build` (`CurrentBuildNumber.UBR`, e.g. `26200.6584`). Null is explicitly shown as `build не определён`; release labels such as 25H2 are not a build source. The field is derived only from successful qualification evidence for the exact revision: the original captured `environment.os_build` remains unchanged for fingerprints, while only its first two numeric components are projected for display. Historical qualification records without embedded environment use their own recorded qualification-control Attempt report, not arbitrary sample reports or current profile metadata. Profile lists, admin revision lists and Job/Run list/detail projections expose this consistently in bounded single SQL queries; historical Job settings/revision IDs are not rewritten.
Windows names are entered manually, for example `Windows 11 Pro — build 26200.6584`. Neither qualification nor an OS update changes that label; only an explicit profile-name edit does. Interfaces render `Profile.name` and the immutable `Run.profile_name` verbatim. Actual `report.environment.os_build` remains technical telemetry for reports/fingerprints/drift, never a naming source. Migration freezes legacy Runs at their currently known profile name before subsequent edits; it cannot reconstruct names that were never stored and does not invent historical builds. Revisions retain separate identifiers and technical metadata, not generated build-qualified names.
Admin: `GET /admin/users`; `POST /admin/users` `{username,password,role}`; `PATCH /admin/users/{id}` `{role?,disabled?,password?}`. User `{id,username,role,disabled,created_at}`.
`GET /admin/profiles`; `POST /admin/profiles` `{name,os,architecture}` -> Profile (maintenance, unqualified). `PATCH /admin/profiles/{id}` `{name?,enabled?}`. `POST /admin/profiles/{id}/maintenance` closes admission, returns profile plus drain status; never powers off live master. `POST /admin/profiles/{id}/publish` `{revision_id}` selects a previously worker-validated stopped source revision; never accepts VMID from browser. `POST /admin/profiles/{id}/qualify` queues qualification, returns `{id,status}`. Qualified revision configuration and credentials are worker-only CLI/file-backed, not browser secrets.
+2 -2
View File
@@ -116,6 +116,6 @@ Grub reserves one additional `max_artifact_bytes` bucket in the existing owner a
Ordinary per-file errors and best-effort changed snapshots make Grub `partial`/`empty`, independently of Defender verdict and telemetry. They do not by themselves retain clones. Missing export, corruption, timeout or private persistence failure does retain stopped evidence. No Grub archive is created for historical jobs without paths. Guest administrator tampering remains within the documented untrusted guest-evidence boundary; Grub files must never be treated as trusted code.
## Windows build identity
## Manual Windows names and technical telemetry
The signed baseline already reads actual `CurrentBuildNumber`, `UBR` and `BuildLabEx` into `environment.os_build`; no release-to-build lookup or manual VM-name rename is used. Qualification persists that observed environment on its exact revision. API/UI build labels project only `CurrentBuildNumber.UBR`, preserving the full original fingerprinted string. Older successful revisions resolve through their recorded qualification control attempts for the same revision. Unknown/unqualified builds remain null, and a historical Run never borrows the current profile's build after maintenance or publication of another revision.
Profile names are fixed, manually editable full labels. Include the desired build literally when naming an environment; there is no automatic build suffix or build override layer. New normal and qualification Runs snapshot that exact name, and later profile edits do not rename historical Runs. Legacy migration freezes the currently known name without inferring past builds. The signed baseline still captures actual `CurrentBuildNumber`, `UBR` and `BuildLabEx` in `environment.os_build` for fingerprint/drift checks and technical reports only; telemetry never rewrites display names.
+6 -13
View File
@@ -9,13 +9,6 @@ import (
"otche/internal/store"
)
// Build labels come only from successful qualification of this exact revision.
// Older revisions retain their recorded control-attempt evidence; no current
// profile metadata or arbitrary sample report may rewrite a historical build.
const revisionOSBuild = `CASE WHEN v.qualification->>'status'='passed' THEN substring(COALESCE(v.qualification#>>'{environment,os_build}',(SELECT a.report#>>'{environment,os_build}' FROM jsonb_array_elements(CASE WHEN jsonb_typeof(v.qualification->'controls')='array' THEN v.qualification->'controls' ELSE '[]'::jsonb END) WITH ORDINALITY c(control,ordinal) JOIN attempts a ON a.id=CASE WHEN c.control->>'attempt_id' ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' THEN (c.control->>'attempt_id')::uuid ELSE NULL END JOIN runs qr ON qr.id=a.run_id WHERE qr.revision_id=v.id AND c.ordinal<=4 AND a.report#>>'{environment,os_build}' IS NOT NULL ORDER BY c.ordinal LIMIT 1)) FROM '^([0-9]+\.[0-9]+)(\.|$)') ELSE NULL END`
const profileProjection = `to_jsonb(p)||jsonb_build_object('os_build',(SELECT ` + revisionOSBuild + ` FROM revisions v WHERE v.id=p.current_revision_id))`
const runProjection = `to_jsonb(r)||jsonb_build_object('profile_name',p.name,'os_build',(SELECT ` + revisionOSBuild + ` FROM revisions v WHERE v.id=r.revision_id))`
func (s *Server) adminRoutes() {
m := s.mux
m.HandleFunc("GET /api/v1/admin/users", s.protected(true, func(w http.ResponseWriter, r *http.Request) {
@@ -24,7 +17,7 @@ func (s *Server) adminRoutes() {
m.HandleFunc("POST /api/v1/admin/users", s.protected(true, s.createUser))
m.HandleFunc("PATCH /api/v1/admin/users/{id}", s.protected(true, s.updateUser))
m.HandleFunc("GET /api/v1/admin/profiles", s.protected(true, func(w http.ResponseWriter, r *http.Request) {
s.list(w, r, `SELECT `+profileProjection+` FROM profiles p ORDER BY name`)
s.list(w, r, `SELECT to_jsonb(p) FROM profiles p ORDER BY name`)
}))
m.HandleFunc("POST /api/v1/admin/profiles", s.protected(true, s.createProfile))
m.HandleFunc("PATCH /api/v1/admin/profiles/{id}", s.protected(true, s.updateProfile))
@@ -32,7 +25,7 @@ func (s *Server) adminRoutes() {
m.HandleFunc("POST /api/v1/admin/profiles/{id}/publish", s.protected(true, s.publish))
m.HandleFunc("POST /api/v1/admin/profiles/{id}/qualify", s.protected(true, s.qualify))
m.HandleFunc("GET /api/v1/admin/profiles/{id}/revisions", s.protected(true, func(w http.ResponseWriter, r *http.Request) {
s.list(w, r, `SELECT to_jsonb(v)-'source_ref'||jsonb_build_object('os_build',`+revisionOSBuild+`) FROM revisions v WHERE profile_id::text=$1 ORDER BY created_at DESC`, r.PathValue("id"))
s.list(w, r, `SELECT to_jsonb(v)-'source_ref' FROM revisions v WHERE profile_id::text=$1 ORDER BY created_at DESC`, r.PathValue("id"))
}))
m.HandleFunc("GET /api/v1/admin/profiles/{id}/qualifications", s.protected(true, func(w http.ResponseWriter, r *http.Request) {
s.list(w, r, `SELECT to_jsonb(q) FROM qualifications q WHERE profile_id::text=$1 ORDER BY created_at DESC`, r.PathValue("id"))
@@ -160,7 +153,7 @@ func (s *Server) createProfile(w http.ResponseWriter, r *http.Request) {
fail(w, 400, "invalid_profile", "Name, OS and x64/x86 architecture required")
return
}
v, e := s.queryObject(r.Context(), `WITH p AS (INSERT INTO profiles(id,name,os,architecture) VALUES($1,$2,$3,$4) RETURNING *) SELECT `+profileProjection+` FROM p`, store.NewID(), in.Name, in.OS, in.Architecture)
v, e := s.queryObject(r.Context(), `INSERT INTO profiles(id,name,os,architecture) VALUES($1,$2,$3,$4) RETURNING to_jsonb(profiles)`, store.NewID(), in.Name, in.OS, in.Architecture)
if e != nil {
s.dbError(w, e)
return
@@ -179,7 +172,7 @@ func (s *Server) updateProfile(w http.ResponseWriter, r *http.Request) {
fail(w, 400, "invalid_profile", "Invalid name")
return
}
v, e := s.queryObject(r.Context(), `WITH p AS (UPDATE profiles SET name=COALESCE($2,name),enabled=COALESCE($3,enabled) WHERE id::text=$1 RETURNING *) SELECT `+profileProjection+` FROM p`, r.PathValue("id"), in.Name, in.Enabled)
v, e := s.queryObject(r.Context(), `UPDATE profiles SET name=COALESCE($2,name),enabled=COALESCE($3,enabled) WHERE id::text=$1 RETURNING to_jsonb(profiles)`, r.PathValue("id"), in.Name, in.Enabled)
if e != nil {
s.dbError(w, e)
return
@@ -187,7 +180,7 @@ func (s *Server) updateProfile(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, 200, v)
}
func (s *Server) maintenance(w http.ResponseWriter, r *http.Request) {
v, e := s.queryObject(r.Context(), `WITH p AS (UPDATE profiles SET state='maintenance',enabled=false,reason='Maintenance admission closed; drain queued clones before changing source disks' WHERE id::text=$1 RETURNING *) SELECT `+profileProjection+` FROM p`, r.PathValue("id"))
v, e := s.queryObject(r.Context(), `UPDATE profiles SET state='maintenance',enabled=false,reason='Maintenance admission closed; drain queued clones before changing source disks' WHERE id::text=$1 RETURNING to_jsonb(profiles)`, r.PathValue("id"))
if e != nil {
s.dbError(w, e)
return
@@ -249,7 +242,7 @@ func (s *Server) publish(w http.ResponseWriter, r *http.Request) {
s.dbError(w, e)
return
}
v, e := s.queryObject(r.Context(), `SELECT `+profileProjection+` FROM profiles p WHERE id=$1`, profile)
v, e := s.queryObject(r.Context(), `SELECT to_jsonb(p) FROM profiles p WHERE id=$1`, profile)
if e != nil {
s.dbError(w, e)
return
+65 -37
View File
@@ -59,7 +59,7 @@ func TestJobSummariesIncludeAssignedProfile(t *testing.T) {
if _, err = db.Exec(ctx, `INSERT INTO jobs(id,owner_id,upload_id,execution_filename,status,settings) VALUES($1,$2,$3,'panel-bat-smoke.bat','completed','{}')`, job, owner, upload); err != nil {
t.Fatal(err)
}
if _, err = db.Exec(ctx, `INSERT INTO runs(id,job_id,profile_id,revision_id,status) VALUES($1,$2,$3,$4,'completed')`, run, job, profile, revision); err != nil {
if _, err = db.Exec(ctx, `INSERT INTO runs(id,job_id,profile_id,revision_id,profile_name,status) SELECT $1,$2,id,$4,name,'completed' FROM profiles WHERE id=$3`, run, job, profile, revision); err != nil {
t.Fatal(err)
}
if _, err = db.Exec(ctx, `INSERT INTO attempts(id,run_id,command_id,phase,outcome,findings,telemetry,cleanup,report) VALUES($1,$2,$3,'finished','executed','not_observed','complete','complete','{"execution":{"exit_code":0}}')`, attempt, run, store.NewID()); err != nil {
@@ -151,17 +151,40 @@ func TestJobSummariesIncludeAssignedProfile(t *testing.T) {
t.Fatalf("job detail lost evidence: %s", detailResponse.Body.String())
}
// Legacy successful qualification keeps its own recorded control evidence.
if _, err = db.Exec(ctx, `UPDATE attempts SET report=report||'{"environment":{"os_build":"26100.1234.26100.1.amd64fre.old"}}'::jsonb WHERE id=$1`, attempt); err != nil {
// Legacy migration freezes the currently known name, never inferred OS data.
if _, err = db.Exec(ctx, `ALTER TABLE runs ALTER COLUMN profile_name DROP NOT NULL; UPDATE runs SET profile_name=NULL`); err != nil {
t.Fatal(err)
}
if _, err = db.Exec(ctx, `UPDATE revisions SET qualification=jsonb_build_object('status','passed','controls',jsonb_build_array(jsonb_build_object('attempt_id','not-a-uuid'),jsonb_build_object('attempt_id',$2::text))) WHERE id=$1`, revision, attempt); err != nil {
if err = store.Migrate(ctx, db); err != nil {
t.Fatal(err)
}
newRevision := store.NewID()
if _, err = db.Exec(ctx, `INSERT INTO revisions(id,profile_id,source_ref,fingerprint,config_digest,qualification) VALUES($1,$2,'win11','new-fingerprint','new-config','{"status":"passed","environment":{"os_build":"26200.6584.26100.1.amd64fre.new"}}')`, newRevision, profile); err != nil {
if _, err = db.Exec(ctx, `UPDATE attempts SET report=report||'{"environment":{"os_build":"99999.9999.unrelated-telemetry"}}'::jsonb WHERE id=$1`, attempt); err != nil {
t.Fatal(err)
}
var session struct {
CSRF string `json:"csrf_token"`
}
if err = json.Unmarshal(logged.Body.Bytes(), &session); err != nil {
t.Fatal(err)
}
writeRequest := func(method, path string, body any) map[string]any {
t.Helper()
encoded, _ := json.Marshal(body)
req := httptest.NewRequest(method, path, strings.NewReader(string(encoded)))
req.AddCookie(cookie)
req.Header.Set("Origin", "http://localhost")
req.Header.Set("X-CSRF-Token", session.CSRF)
res := httptest.NewRecorder()
s.ServeHTTP(res, req)
if res.Code != 200 && res.Code != 201 {
t.Fatalf("%s %s: %d %s", method, path, res.Code, res.Body)
}
var value map[string]any
if err := json.Unmarshal(res.Body.Bytes(), &value); err != nil {
t.Fatal(err)
}
return value
}
get := func(endpoint string) map[string]any {
t.Helper()
req := httptest.NewRequest("GET", endpoint, nil)
@@ -177,38 +200,43 @@ func TestJobSummariesIncludeAssignedProfile(t *testing.T) {
}
return body
}
for _, current := range []string{revision, newRevision} {
if _, err = db.Exec(ctx, `UPDATE profiles SET current_revision_id=$2 WHERE id=$1`, profile, current); err != nil {
t.Fatal(err)
}
expectedProfileBuild := "26100.1234"
if current == newRevision {
expectedProfileBuild = "26200.6584"
}
for _, endpoint := range []string{"/api/v1/profiles", "/api/v1/admin/profiles"} {
item := get(endpoint)["items"].([]any)[0].(map[string]any)
if item["os_build"] != expectedProfileBuild || item["name"] != "Windows 11 Pro 25H2 - VM7001" {
t.Fatalf("profile build/name changed incorrectly: %v", item)
}
}
for _, endpoint := range []string{"/api/v1/jobs/" + job, "/api/v1/jobs", "/api/v1/dashboard"} {
body := get(endpoint)
if endpoint == "/api/v1/jobs" {
body = body["items"].([]any)[0].(map[string]any)
}
if endpoint == "/api/v1/dashboard" {
body = body["recent_jobs"].([]any)[0].(map[string]any)
}
run := body["runs"].([]any)[0].(map[string]any)
if run["revision_id"] != revision || run["os_build"] != "26100.1234" {
t.Fatalf("historical run borrowed current profile build: %v", run)
}
}
}
if _, err = db.Exec(ctx, `UPDATE revisions SET qualification=jsonb_set(qualification,'{status}','"failed"') WHERE id=$1`, newRevision); err != nil {
manualName := "Windows lab — build 26200.6584"
writeRequest("PATCH", "/api/v1/admin/profiles/"+profile, map[string]any{"name": manualName})
if err = store.Migrate(ctx, db); err != nil {
t.Fatal(err)
}
if get("/api/v1/profiles")["items"].([]any)[0].(map[string]any)["os_build"] != nil {
t.Fatal("failed qualification presented as authoritative Windows build")
for _, endpoint := range []string{"/api/v1/profiles", "/api/v1/admin/profiles"} {
item := get(endpoint)["items"].([]any)[0].(map[string]any)
if item["name"] != manualName {
t.Fatalf("manual name was reinterpreted: %v", item)
}
}
for _, endpoint := range []string{"/api/v1/jobs/" + job, "/api/v1/jobs", "/api/v1/dashboard"} {
body := get(endpoint)
if endpoint == "/api/v1/jobs" {
body = body["items"].([]any)[0].(map[string]any)
}
if endpoint == "/api/v1/dashboard" {
body = body["recent_jobs"].([]any)[0].(map[string]any)
}
run := body["runs"].([]any)[0].(map[string]any)
if run["profile_name"] != "Windows 11 Pro 25H2 - VM7001" || run["revision_id"] != revision {
t.Fatalf("historical name/revision changed after rename: %v", run)
}
}
if _, err = db.Exec(ctx, `INSERT INTO bindings(owner_id,pool,iso_storage,disk_storage,node,configured,isolation_expires_at) VALUES($1,'test','test','test','test',true,now()+interval '1 hour')`, owner); err != nil {
t.Fatal(err)
}
if _, err = db.Exec(ctx, `INSERT INTO heartbeats(name,seen_at,ready) VALUES('worker',now(),true)`); err != nil {
t.Fatal(err)
}
created := writeRequest("POST", "/api/v1/jobs", map[string]any{"upload_id": upload, "profile_ids": []string{profile}, "settings": Settings{Internet: "offline", Duration: 30, Filename: "original", Privilege: "user", ArgsMode: "none", Args: []string{}}})
createdID := created["id"].(string)
if created["runs"].([]any)[0].(map[string]any)["profile_name"] != manualName {
t.Fatal("new run did not snapshot explicitly entered name")
}
writeRequest("PATCH", "/api/v1/admin/profiles/"+profile, map[string]any{"name": "Another explicitly entered label"})
if get("/api/v1/jobs/" + createdID)["runs"].([]any)[0].(map[string]any)["profile_name"] != manualName {
t.Fatal("queued run name followed later profile edit")
}
}
+4 -4
View File
@@ -140,7 +140,7 @@ func (s *Server) jobRoutes() {
m.HandleFunc("GET /api/v1/artifacts/{id}/content", s.protected(false, s.artifactContent))
m.HandleFunc("GET /api/v1/attempts/{id}/video", s.protected(false, s.video))
m.HandleFunc("GET /api/v1/profiles", s.protected(false, func(w http.ResponseWriter, r *http.Request) {
s.list(w, r, `SELECT `+profileProjection+` FROM profiles p ORDER BY name`)
s.list(w, r, `SELECT to_jsonb(p) FROM profiles p ORDER BY name`)
}))
m.HandleFunc("GET /api/v1/dashboard", s.protected(false, s.dashboard))
}
@@ -347,7 +347,7 @@ func (s *Server) createJob(w http.ResponseWriter, r *http.Request) {
}
for _, p := range in.ProfileIDs {
run := store.NewID()
if _, e = tx.Exec(r.Context(), `INSERT INTO runs(id,job_id,profile_id,revision_id)VALUES($1,$2,$3,$4)`, run, id, p, revisions[p]); e == nil {
if _, e = tx.Exec(r.Context(), `INSERT INTO runs(id,job_id,profile_id,revision_id,profile_name) SELECT $1,$2,id,$4,name FROM profiles WHERE id=$3`, run, id, p, revisions[p]); e == nil {
_, e = tx.Exec(r.Context(), `INSERT INTO attempts(id,run_id,command_id)VALUES($1,$2,$3)`, store.NewID(), run, store.NewID())
}
if e != nil {
@@ -392,9 +392,9 @@ func (s *Server) admit(r *http.Request, tx pgx.Tx, owner string) error {
return nil
}
const jobRunsSummary = `COALESCE((SELECT jsonb_agg(` + runProjection + ` ORDER BY r.id) FROM runs r JOIN profiles p ON p.id=r.profile_id WHERE r.job_id=j.id),'[]'::jsonb)`
const jobRunsSummary = `COALESCE((SELECT jsonb_agg(to_jsonb(r) ORDER BY r.id) FROM runs r WHERE r.job_id=j.id),'[]'::jsonb)`
const jobQuery = `SELECT to_jsonb(j)||jsonb_build_object('filename',u.filename,'sha256',u.sha256,'size',u.size,'runs',COALESCE((SELECT jsonb_agg(` + runProjection + `||jsonb_build_object('attempts',COALESCE((SELECT jsonb_agg((to_jsonb(a)-'lease_owner'-'lease_until'-'release_requested')||jsonb_build_object('allocation',CASE WHEN $3='admin' THEN(SELECT jsonb_build_object('id',v.id,'node',v.node,'vmid',v.vmid,'state',v.state) FROM allocations v WHERE v.attempt_id=a.id) ELSE NULL END,'artifacts',COALESCE((SELECT jsonb_agg(to_jsonb(ar)-'storage_key'||jsonb_build_object('url','/api/v1/artifacts/'||ar.id||'/content')) FROM artifacts ar WHERE ar.attempt_id=a.id),'[]'::jsonb)) ORDER BY a.created_at) FROM attempts a WHERE a.run_id=r.id),'[]'::jsonb))) FROM runs r JOIN profiles p ON p.id=r.profile_id WHERE r.job_id=j.id),'[]'::jsonb)) FROM jobs j JOIN uploads u ON u.id=j.upload_id WHERE j.id::text=$1 AND(j.owner_id=$2 OR $3='admin')`
const jobQuery = `SELECT to_jsonb(j)||jsonb_build_object('filename',u.filename,'sha256',u.sha256,'size',u.size,'runs',COALESCE((SELECT jsonb_agg(to_jsonb(r)||jsonb_build_object('attempts',COALESCE((SELECT jsonb_agg((to_jsonb(a)-'lease_owner'-'lease_until'-'release_requested')||jsonb_build_object('allocation',CASE WHEN $3='admin' THEN(SELECT jsonb_build_object('id',v.id,'node',v.node,'vmid',v.vmid,'state',v.state) FROM allocations v WHERE v.attempt_id=a.id) ELSE NULL END,'artifacts',COALESCE((SELECT jsonb_agg(to_jsonb(ar)-'storage_key'||jsonb_build_object('url','/api/v1/artifacts/'||ar.id||'/content')) FROM artifacts ar WHERE ar.attempt_id=a.id),'[]'::jsonb)) ORDER BY a.created_at) FROM attempts a WHERE a.run_id=r.id),'[]'::jsonb))) FROM runs r WHERE r.job_id=j.id),'[]'::jsonb)) FROM jobs j JOIN uploads u ON u.id=j.upload_id WHERE j.id::text=$1 AND(j.owner_id=$2 OR $3='admin')`
func (s *Server) respondJob(w http.ResponseWriter, r *http.Request, id string, status int) {
a := user(r)
+3
View File
@@ -25,3 +25,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS allocations_active_vmid ON allocations(vmid) W
CREATE TABLE IF NOT EXISTS extractor_allocations(id uuid PRIMARY KEY, attempt_id uuid UNIQUE NOT NULL REFERENCES attempts(id), owner_id uuid NOT NULL REFERENCES users(id), node text NOT NULL, vmid integer NOT NULL CHECK(vmid NOT IN(7000,7001)), state text NOT NULL, upid text, disk_slot text NOT NULL, disk_serial text NOT NULL, disk_bytes bigint NOT NULL, metadata jsonb NOT NULL DEFAULT '{}');
CREATE UNIQUE INDEX IF NOT EXISTS extractor_active_vmid ON extractor_allocations(vmid) WHERE state <> 'deleted';
ALTER TABLE bindings ADD COLUMN IF NOT EXISTS isolation_expires_at timestamptz;
ALTER TABLE runs ADD COLUMN IF NOT EXISTS profile_name text;
UPDATE runs r SET profile_name=p.name FROM profiles p WHERE r.profile_id=p.id AND r.profile_name IS NULL;
ALTER TABLE runs ALTER COLUMN profile_name SET NOT NULL;
+1 -1
View File
@@ -52,7 +52,7 @@ func TestLeaseGenerationFencesExpiredClaimAndAllowsCancelledCleanup(t *testing.T
{"INSERT INTO revisions(id,profile_id,source_ref,fingerprint,config_digest)VALUES($1,$2,'source','fp','cfg')", []any{revision, profile}},
{"INSERT INTO uploads(id,owner_id,filename,size,sha256,storage_key)VALUES($1,$2,'safe.exe',1,'hash','test-upload')", []any{upload, owner}},
{"INSERT INTO jobs(id,owner_id,upload_id,execution_filename,settings)VALUES($1,$2,$3,'safe.exe','{}')", []any{job, owner, upload}},
{"INSERT INTO runs(id,job_id,profile_id,revision_id)VALUES($1,$2,$3,$4)", []any{run, job, profile, revision}},
{"INSERT INTO runs(id,job_id,profile_id,revision_id,profile_name)VALUES($1,$2,$3,$4,'Lease fixture')", []any{run, job, profile, revision}},
{"INSERT INTO attempts(id,run_id,command_id)VALUES($1,$2,$3)", []any{aid, run, command}},
}
for _, s := range statements {
+2 -4
View File
@@ -158,7 +158,7 @@ func (e *engine) enqueueControl(ctx context.Context, tx pgx.Tx, qid, profile, re
if _, err = tx.Exec(ctx, "INSERT INTO jobs(id,owner_id,upload_id,execution_filename,settings) VALUES($1,$2,$3,$4,$5)", c.JobID, owner, upload, filename, settings); err != nil {
return c, err
}
if _, err = tx.Exec(ctx, "INSERT INTO runs(id,job_id,profile_id,revision_id) VALUES($1,$2,$3,$4)", run, c.JobID, profile, revision); err != nil {
if _, err = tx.Exec(ctx, "INSERT INTO runs(id,job_id,profile_id,revision_id,profile_name) SELECT $1,$2,id,$4,name FROM profiles WHERE id=$3", run, c.JobID, profile, revision); err != nil {
return c, err
}
_, err = tx.Exec(ctx, "INSERT INTO attempts(id,run_id,command_id) VALUES($1,$2,$3)", c.AttemptID, run, command)
@@ -171,7 +171,6 @@ func (e *engine) completeQualification(ctx context.Context, id, profile string,
}
baseline := ""
var metadata any
var environment json.RawMessage
onlineControls := map[string]bool{}
for _, control := range result.Controls {
if control.Internet == "online" {
@@ -238,7 +237,6 @@ func (e *engine) completeQualification(ctx context.Context, id, profile string,
result.Errors = append(result.Errors, "Control clones did not share identical source/Defender baseline")
}
baseline = ready.Baseline.Fingerprint
environment = ready.Environment
metadata = map[string]any{"environment": json.RawMessage(ready.Environment), "defender": json.RawMessage(ready.Defender), "baseline_fingerprint": baseline}
}
status, state := "passed", "qualified"
@@ -247,7 +245,7 @@ func (e *engine) completeQualification(ctx context.Context, id, profile string,
}
result.Baseline = baseline
encoded, _ := json.Marshal(result)
q, _ := json.Marshal(map[string]any{"worker_validated": true, "status": status, "state": state, "baseline_fingerprint": baseline, "qualification_id": id, "controls": result.Controls, "errors": result.Errors, "environment": environment})
q, _ := json.Marshal(map[string]any{"worker_validated": true, "status": status, "state": state, "baseline_fingerprint": baseline, "qualification_id": id, "controls": result.Controls, "errors": result.Errors})
meta, _ := json.Marshal(metadata)
tx, err := e.db.Begin(ctx)
if err != nil {