GitHub
11/08/2026, 7:24 AMAdminTag gains Sensitive bool (column sensitive, not null, default false).
• A node is sensitive if any live tag on it is sensitive. Deleting the tag, or removing it from the node, stops the node counting as sensitive.
• Environment admins set the flag through the existing tag add/edit endpoint ("sensitive": true), which is already env-admin only. The tag action audit entry names the change.
One filter. handlers.NodeScope.Apply now excludes sensitive nodes at the standard and tags tiers. Full is unchanged.
• The exclusion is NULL-safe, and the boolean is bound as a parameter so it works on both sqlite and Postgres.
• Every path built on the filter in part 2 picks this up: targeting, node lists, node detail, logs, posture, cancellation and hosted MCP.
Query results. The log readers' include-only UUID list becomes logging.NodeFilter{Only, Except}. This applies to both the DB and S3 readers, for JSON and CSV.
• At standard tier, Except lists every sensitive node UUID. This includes deleted sensitive nodes, which are matched through a new ArchiveOsqueryNode.NodeID column.
• An empty Except drops nothing. An empty Only returns nothing.
• Results are judged against the caller's lowest tier across the query's environment and every environment it targets. So a query in env A that targets env B can't show B's sensitive rows to someone who is only standard in B.
Carves. At standard tier, files from sensitive nodes are hidden. Deleting a node keeps its tag rows, so a deleted sensitive node's carves stay hidden.
Grants and pending work.
• A sensitive tag can't be a scope tag (400 sensitive tag cannot be a scope tag).
• Marking a tag sensitive removes it from every scope grant.
• Marking a tag sensitive, or tagging a node with a sensitive tag, cancels pending queries and carves from creators who can no longer reach that node. Env admins' and full-tier users' pending work is kept.
Other routes.
• Per-node activity and tiles stats now resolve by UUID only and return the same 404 for hidden and missing nodes. The batch versions drop hidden nodes.
• The env error-node list is filtered.
• The cross-environment node lookup (read-only admin) is filtered per environment.
• Query and carve detail blank UUID/hostname targets at standard tier unless they name a live, non-sensitive node.
Part 2 gap fixed. Node detail, logs and posture (through visibleNode), plus the unpaged active/inactive/all node lists, used to filter only at the tags tier. They now filter whenever the tier is below full.
Docs and clients. OpenAPI is updated for the tag sensitive field. The frontend tag types gain sensitive (types only; the UI comes in part 5). The MCP get_query_results description is updated.
Security and operational impact
• Nothing is sensitive on upgrade day, so nobody loses or gains access until a tag is marked.
• Everything is enforced in osctrl-api. osctrl-tls is not changed.
• A sensitive node returns the same 404 as a node that doesn't exist on every node-scoped route below full tier.
• Schema changes are additive:
• admin_tags.sensitive (Postgres: ADD COLUMN ... NOT NULL DEFAULT false).
• archive_osquery_nodes.node_id, with an index built at startup on a table that only grows.
• Aggregate counts (dashboard stats, platform and version counts, query execution counts) still include sensitive nodes.
Known limits
• Archives from before this change: nodes archived earlier have no node_id. If one of their tags is later marked sensitive, their old query results stay visible at standard tier.
• Multi-environment queries: a user with no access to one of a query's target environments sees no results for that query.
• Duplicate UUIDs: a UUID enrolled in two environments can be over-hidden.
• CLI: the CLI can't set the sensitive flag yet. Tagging through the CLI's direct-database mode doesn't cancel pending work, although results are still hidden when read.
• Audit log and alert history: these stay fully visible to read-only admins, sensitive node identities included, because the role exists for oversight.
• Environment rename: earlier queries' stored environment targets keep the old name, so their results show zero rows after a rename. This fails closed and will be fixed in a follow-up.
Validation
• go build ./... and go test ./... pass.
• New tests cover:
• the filter: standard and tags exclusion, deleted sensitive tags, soft-deleted taggings, deleted nodes;
• NodeFilter empty and combined cases on the DB and S3 readers;
• results and carves at standard tier, including deleted nodes, environment rename and cross-environment queries;
• tag add/edit with the flag, grant removal, pending cancellation and scope-tag rejection;
• 404 parity on node detail, logs, posture and per-node stats, batch filtering, error nodes and lookup;
• target blanking.
• make openapi-check passes.
• cd frontend && npm run check passes.
• `golangci-lint`: no new findings.
• git diff --stat -- cmd/tls is empty.
• Not run: Playwright and in-browser checks. There is no visible UI change, so there are no screenshots.
jmpsec/osctrlGitHub
11/08/2026, 7:55 AM