yuzu

update_source_trust

What it doesPackage and update-source trust posture (facts only)
Version1.0.0
KindCollector · read-only · gathered (crossplatform.security.update_source_trust)
PlatformsWindows 🟡 planned · macOS 🟡 planned · Linux ✅
Actionssources (definition crossplatform.security.update_source_trust)
Securitysecurable Security · operation Read · risk Low · dispatch ReadOnly · approval gate None
Rolesexecute: endpoint-admin, endpoint-operator, security-admin · author: content-author

How it works

sources reports facts about how the device’s package and update sources are configured to trust their signing authorities. It reads local configuration files and nothing else: no subprocess, no network fetch, no write. The first row is always a status row saying whether the read was complete; the data rows follow, one per source line or stanza and one per keyring file. It reports what is configured and leaves any judgement, and any enforcement, to the consumer and to the sibling posture plugins.

On Linux the plugin reads the apt family. Source files come from /etc/apt/sources.list and /etc/apt/sources.list.d/ in both the one-line and the deb822 format, and each row carries the signed-by, trusted, enabled and (one-line only) allow-insecure settings. The keyrings in /etc/apt/trusted.gpg, /etc/apt/trusted.gpg.d/ and /etc/apt/keyrings/ are listed with their format and size; key material is never emitted. The plugin also lists /etc/yum.repos.d by name without opening any .repo file, and when that directory has entries the result is CONSTRAINED with linux:rpm_repo:planned, so an rpm host never reads as having no sources. On macOS and Windows the plugin reads nothing and answers with one unsupported status row (macos:planned or windows:planned).

Reads are bounded and conservative. A symlink as the final component of a file or directory is refused, never followed. A source file over 1 MiB, a directory over 1,024 entries, rows beyond a 1 MiB total, or more than 16 MiB read in one run stops that part of the walk and is reported, never silently truncated. In sources.list.d only the files apt itself reads are reported (not hidden, names within [A-Za-z0-9_.:-], suffix .list or .sources). Keyrings are chosen by suffix alone (*.gpg and *.asc in trusted.gpg.d, any name in /etc/apt/keyrings), because apt before 3.0 trusts a key under any such name; apt 3.0 and later skip odd names there, so an extra row can appear on those hosts. An entry apt itself would refuse is counted as unparsed_entry and produces no row. A NUL or non-UTF-8 byte in a reported field is replaced with ? and flagged invalid_bytes. A file whose size or modification time changes while it is read, or that is empty and was modified within the last two seconds, is flagged modified_during_read instead of being reported as read.

flowchart LR
  OP["Operator or workflow"] -->|"authz: Security Read"| SRV["Server"]
  SRV -->|"gRPC mTLS"| HOST["Agent plugin host"] --> EX["update_source_trust execute"]
  EX --> WIN["Windows: status row only, windows:planned"]
  EX --> MAC["macOS: status row only, macos:planned"]
  EX --> LIN["Linux: apt sources and keyrings, yum.repos.d listed by name"]
  WIN --> ROWS["rows and typed result status"]
  MAC --> ROWS
  LIN --> ROWS
  ROWS --> RS[("ResponseStore")] --> API["REST /api/responses"]

What you can decide with it

Each data row is one fact an operator can act on. On Linux (apt hosts):

QuestionWhere to lookReading it
Is signature checking switched off for a source?apt_source fields 8 and 9 (trusted, allow_insecure)yes means apt skips signature checking for that source. unset means the source does not say; it does not prove checking is enforced, because global apt settings are not read.
Does a source pin its own key, or rely on every globally trusted key?apt_source field 7 (signed_by)A path or fingerprint pins the source to that key. - means apt falls back to its global trusted keyrings. inline_key means the key is embedded in the file.
Which keys does apt trust for every source?apt_keyring rows with scope legacy_trusted_gpg or trusted_gpg_dEvery key in those files is a global trust anchor. Rows in scope etc_apt_keyrings are only trusted where a signed-by names them.
Where does this host install software from?apt_source URIs, suites, componentsThe URI list, with any user:pass@ redacted. A disabled source shows enabled = no.

What it cannot tell you: who owns a keyring file or whether its key is valid (no fingerprint is computed), whether a URI is reachable or legitimate, anything about rpm/dnf, zypper, pacman or apk hosts, or the macOS and Windows update policy. A clean result (OK, zero or more rows, no trusted=yes) proves only that the apt source files that were read do not disable verification; it does not prove an apt host is hardened, and zero rows on a non-apt host says nothing. There is no workflow to follow: the action is read-only and takes no parameters.

Diagrams

flowchart TD
  A["Candidate file or directory under /etc/apt"] --> B{"Exists?"}
  B -->|"no"| Z["Zero rows, no token, stays supported"]
  B -->|"yes"| C{"Final component a symlink, or not a regular file?"}
  C -->|"yes"| T1["Token symlink_refused, not_a_directory or not_regular"]
  C -->|"no"| D{"Over 1 MiB, or run already past 16 MiB read?"}
  D -->|"yes"| T2["Token oversized or input_cap, stop that part"]
  D -->|"no"| E["Read between two size and time checks"]
  E --> F{"Changed while read?"}
  F -->|"yes"| T3["Token short_read or modified_during_read"]
  F -->|"no"| G{"Entry parses the way apt parses it?"}
  G -->|"no"| T4["Token unparsed_entry, no row"]
  G -->|"yes"| H["Row emitted, within the 1 MiB row budget"]
  T1 --> R["Result is CONSTRAINED with every token"]
  T2 --> R
  T3 --> R
  T4 --> R

The diagram follows one candidate file through the Linux read. A file that is merely absent is not a failure, while every other way of not reading it completely becomes a token that turns the whole result into CONSTRAINED, so a source that could not be read is never reported as absent or unsigned. Rows that were read are still returned alongside the tokens.

OS capability

ActionWindowsmacOSLinux
sources🟡 planned · rung 1 · HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate{,\AU} registry values🟡 planned · rung 1 · CFPropertyListCreateWithData over /Library/Preferences and /Library/Managed Preferences com.apple.SoftwareUpdate.plist✅ supported · rung 1 · /etc/apt/sources.list{,.d/} (one-line + deb822), /etc/apt/trusted.gpg{,.d/} and /etc/apt/keyrings/* config file reads

Declared limits per leg (descriptor fallback text, verbatim):

Privileges and prerequisites

OSRuns asExtra grant neededMeasuredIf the read is refused
Windowsn/a — the leg reads nothing (caveat 4)n/an/an/a — the placeholder reports unsupported with windows:planned
macOSn/a — the leg reads nothing (caveat 4)n/an/an/a — the placeholder reports unsupported with macos:planned
Linuxagent daemon, dedicated unprivileged account (yuzu-agent on the packaged unit), never root by design (docs/agent-privilege-model.md)None to readNot measured on the yuzu-agent account: the only capture (docs/samples/linux.txt) ran as root in a container, which bypasses permission bits, so it proves the read path and the row shapes, not the grantCONSTRAINED / partial with a linux:apt_sources:<detail> or linux:apt_keyring:<detail> token (or linux:rpm_repo:<detail> when the /etc/yum.repos.d listing itself is refused); the unreadable source is absent from the rows, never reported as unsigned or absent

No external binaries, no subprocesses, no shell-out and no network use: the Linux leg is an in-process, bounded read of local files, and the macOS and Windows legs read nothing.

Data contract

Inputs

The action takes no parameters.

Outputs

Every row is pipe-delimited; field 0 is the row kind and the first row is always status|sources|<supported\|constrained\|unsupported>|<reason or ->. The kinds are apt_source and apt_keyring; the rpm/dnf, macOS and Windows legs emit no data rows (caveats 3 and 4). Every row of a kind has the same field count, and - is an absent value. Booleans use one four-value vocabulary: yes, no, unset (the key is absent) and unmodelled (the key is present with a value the plugin does not recognise). A host that has no apt configuration reports zero rows and stays supported: an absent thing is not a failure (the deferred rpm/dnf family is the exception, see Result status). Free text is escaped for the server’s pipe grammar (safe_output_field, lossy on backslash by design) and URL userinfo (user:pass@) is redacted. update_source_trust is not in the server’s key/value plugin set, so rows are decoded as pipe-separated fields, not as key|rest. The columns below are the widest shapes; narrower shapes use a prefix of them. The Available column lists the OSes whose leg emits that field; macOS and Windows read nothing today, so only the status row’s fields (row_kind to field_3) are populated there (caveat 4). A deb822 stanza that lists several types, URIs, suites or components is one row with the values space-separated (bookworm bookworm-updates), and each keyring file is its own row.

crossplatform.security.update_source_trust — row_kind|field_1|field_2|field_3|field_4|field_5|field_6|field_7|field_8|field_9|field_10

FieldTypeValuesAvailableExampleDescription
row_kindstringstatus apt_source apt_keyringWindows, Linux, macOSapt_sourceRow shape discriminator (field 0). Values: status, apt_source, apt_keyring.
field_1string-Windows, Linux, macOS/etc/apt/sources.list.d/debian.sourcesstatus: the literal “sources”. apt_source: the logical absolute path of the source file. apt_keyring: the keyring file path.
field_2string-Windows, Linux, macOSdeb822status: supported, constrained or unsupported (Windows and macOS, whose legs read nothing, and a leg that threw report unsupported). apt_source: format, one_line or deb822. apt_keyring: scope (legacy_trusted_gpg, trusted_gpg_d or etc_apt_keyrings).
field_3string-Windows, Linux, macOSdebstatus: reason token(s) for a constrained or unsupported read, ”-” when complete; a token has the form :: (linux:rpm_repo:planned on a Linux host whose /etc/yum.repos.d has entries) or : (windows:planned on Windows, macos:planned on macOS). apt_source: source types, space-separated when several (deb, deb-src or unmodelled). apt_keyring: key format (armored, binary, empty or unmodelled).
field_4string-Linuxhttps://deb.debian.org/debianapt_source: URIs (space-separated when the entry lists several), userinfo (user:pass@) redacted. apt_keyring: size in bytes. Not used by status.
field_5string-Linuxbookwormapt_source: suites, space-separated when several.
field_6string-Linuxmainapt_source: components.
field_7string-Linux/usr/share/keyrings/debian-archive-keyring.gpgapt_source: signed_by, the Signed-By value (paths and/or fingerprints), the literal inline_key when a deb822 Signed-By embeds a key block (key material is never emitted), the literal unmodelled when a one-line signed-by uses += or -=, or ”-” when absent (apt then uses its global trusted keyrings).
field_8string-Linuxunsetapt_source: trusted (yes, no, unset, unmodelled; yes disables signature checking for the source; only the apt boolean words are recognised and any other value is unmodelled; unset means the source does not say, not that signatures are enforced).
field_9string-Linuxunsetapt_source: allow_insecure (yes, no, unset, unmodelled; read from the one-line [allow-insecure=…] option only: apt ignores it in deb822, so it is unset there).
field_10string-Linuxyesapt_source: enabled (yes, no, unmodelled; deb822 defaults to enabled).

Result status

sources sets OK/FULL when every read completed, including a host with no sources at all. Any read that failed for a reason other than the file being absent (permission, symlink, oversize, short read, a file that changed while it was read, invalid bytes, listing error, entry cap, output cap, input cap, an entry that would not parse) makes the whole result CONSTRAINED/PARTIAL with the failure tokens as the reason, and the same tokens appear in the leading status row. A Linux host whose /etc/yum.repos.d has entries is CONSTRAINED/PARTIAL with linux:rpm_repo:planned: the rpm/dnf family is not read (caveat 3), and the skipped rpm/dnf family never reads as an empty one (families other than apt and rpm/dnf are not detected, caveat 5). Windows and macOS report UNAVAILABLE (caveat 4). No exception crosses the plugin boundary: a leg that throws is reported as one unsupported status row and UNAVAILABLE with an exception token that names only the category, never the exception text.

StatusCompletenessProvenanceWhen
OKfull(empty)Linux: every read completed, populated or genuinely empty (no /etc/yum.repos.d entries). On a host whose sources live outside apt and rpm/dnf (zypper, pacman, apk) zero rows is NOT evidence of no sources (caveat 5)
CONSTRAINEDpartiallinux:apt_sources:<detail> / linux:apt_keyring:<detail> / linux:rpm_repo:<detail>, with <detail> one of permission_denied, symlink_refused, not_a_directory, not_regular, io_error, open_failed, read_failed, dir_open_failed, oversized, short_read, enumeration_error, entry_cap, output_cap, input_cap, invalid_bytes, modified_during_read or unparsed_entry (apt only)a source exists but could not be read completely or decoded; the rows that were read are still emitted. permission_denied: the agent account cannot read the path. symlink_refused / not_a_directory: a final path component is a symbolic link (never followed) or a file sits where a directory belongs. not_regular: a FIFO, socket, device or directory sits where a file belongs. oversized / entry_cap / output_cap: a file over 1 MiB, a directory over 1,024 entries, or rows past the 1 MiB total budget (the rest was not read). input_cap: the walk pulled more than 16 MiB of source and keyring bytes from the kernel, counting failed reads; the rest was not read. short_read / modified_during_read: the file changed while it was read. invalid_bytes: a NUL or non-UTF-8 byte in a reported field after redaction was replaced with ?. unparsed_entry: an entry apt itself would refuse. open_failed / read_failed / dir_open_failed / io_error / enumeration_error: an operating-system operation failed
CONSTRAINEDpartiallinux:rpm_repo:planned/etc/yum.repos.d has entries and the rpm/dnf .repo family is not read (caveat 3); the apt rows that were read are still emitted
UNAVAILABLEunknownwindows:plannedWindows: one status|sources|unsupported|windows:planned row (caveat 4)
UNAVAILABLEunknownmacos:plannedmacOS: one status|sources|unsupported|macos:planned row (caveat 4)
UNAVAILABLEunknownwindows:leg:exception:<category> / linux:leg:exception:<category> / macos:leg:exception:<category>, <category> one of bad_alloc, std_exception, unknown (the exception text is deliberately never carried)a leg threw: one status|sources|unsupported|<os>:leg:exception:<category> row, reported instead of unwinding across the plugin boundary. Under sustained allocation failure the category suffix can be lost, leaving the bare <os>:leg:exception, and the row itself may not be written; the typed status (or a non-zero rc) still reaches the host. A leg that throws after it has already reported (an allocation failure while its rows are being written) leaves this unsupported row BEHIND the rows already written, after the leading supported or constrained row: the one case with two status rows, where the first is not the outcome; the last status row and the typed status are the outcome.

Reading a constrained result

A token names the source family (apt_sources, apt_keyring or rpm_repo), not the file. Every token means a source or field was not read exactly as written; the rows that were read are still returned.

Token detailMeaningWhat to do
permission_deniedthe agent’s account cannot read that pathgrant the account read access, unless the file is root-only on purpose (for example it carries a credential), or accept the gap
symlink_refused, not_a_directorythe final path component is a symbolic link, or a file sits where a directory is expected; the plugin never follows a linkmake it a regular file or directory, or accept the gap (a link that a package manager or configuration tool maintains would drift)
not_regulara FIFO, socket, device or directory sits where a file is expectedremove or replace it
oversized, entry_cap, output_capa file over 1 MiB, a directory over 1,024 entries, or rows past the 1 MiB total budget: the rest was not readreduce the configuration; a real host is a few KiB
input_capthe walk pulled more than 16 MiB of source and keyring bytes from the kernel, counting failed reads; the rest was not readreduce the configuration; a real host is a few KiB
short_read, modified_during_readthe file changed while it was readrun the action again
invalid_bytesa NUL or non-UTF-8 byte in a reported field after redaction was replaced with ?fix the file; the affected field shows ?
planned (linux:rpm_repo:planned)/etc/yum.repos.d has entries and the rpm/dnf family is not read yetnone; every rpm-family host reports this until that leg exists
<os>:leg:exception:<category>the leg threw (bad_alloc, std_exception or unknown); the row is unsupported; the bare <os>:leg:exception means the category could not be appended under allocation failurereport it; nothing was read
unparsed_entryan entry apt itself would reject (an unterminated [, a missing suite)fix the entry
open_failed, read_failed, dir_open_failed, io_error, enumeration_errorthe OS refused or failed an operationcheck the path and the host’s storage

Where the data goes

Sample output

macOS — captured: macos macOS 26.6.2 arm64 · bare-metal · 2026-09-21 · euid 501 · leg-hash 4901149d2e67

== action=sources
status|sources|unsupported|macos:planned
[result_status] UNAVAILABLE / UNKNOWN / macos:planned

Linux — captured: linux Debian GNU/Linux 13 (trixie) aarch64 · container · 2026-09-21 · euid 0 · leg-hash 4901149d2e67

== action=sources
status|sources|supported|-
apt_source|/etc/apt/sources.list.d/debian.sources|deb822|deb|http://deb.debian.org/debian|trixie trixie-updates|main|/usr/share/keyrings/debian-archive-keyring.pgp|unset|unset|yes
apt_source|/etc/apt/sources.list.d/debian.sources|deb822|deb|http://deb.debian.org/debian-security|trixie-security|main|/usr/share/keyrings/debian-archive-keyring.pgp|unset|unset|yes
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-bookworm-automatic.asc|trusted_gpg_d|armored|11861
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-bookworm-security-automatic.asc|trusted_gpg_d|armored|11873
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-bookworm-stable.asc|trusted_gpg_d|armored|461
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-bullseye-automatic.asc|trusted_gpg_d|armored|11861
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-bullseye-security-automatic.asc|trusted_gpg_d|armored|11873
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-bullseye-stable.asc|trusted_gpg_d|armored|3403
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-trixie-automatic.asc|trusted_gpg_d|armored|11861
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-trixie-security-automatic.asc|trusted_gpg_d|armored|11873
apt_keyring|/etc/apt/trusted.gpg.d/debian-archive-trixie-stable.asc|trusted_gpg_d|armored|1384
[result_status] OK / FULL

Caveats and known gaps

  1. No documented customer driver. The capability map’s “Patch Connectivity Testing” row checks that update sources are reachable, not whether they are trustworthy, and the SOC 2 supply-chain material covers Yuzu’s own build pipeline rather than a customer endpoint’s package sources. The plugin exists on capability-gap reasoning alone.
  2. Facts only. The plugin is read-only and reports how sources are configured; an unsigned or unexpected source is a row, not a verdict. It does not enforce, fetch, verify a signature or resolve a key, and enforcement posture belongs to the sibling posture plugins.
  3. rpm/dnf family planned. The yum/dnf .repo settings (gpgcheck, repo_gpgcheck, gpgkey, sslverify) are not read. A Linux host whose /etc/yum.repos.d has entries reports CONSTRAINED with linux:rpm_repo:planned and no rpm_repo rows, so it never reads as having no sources; a host without that directory (Debian, Ubuntu) stays supported.
  4. macOS and Windows legs planned. The macOS Software Update policy (local and MDM-managed com.apple.SoftwareUpdate.plist) and the Windows WSUS and Automatic Updates policy (HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate) are not read; each leg reports one unsupported status row.
  5. Known limits of the apt read. A Signed-By key is reported by path or fingerprint text, not resolved, and /usr/share/keyrings is not inventoried; global apt configuration (apt.conf, apt.conf.d) is not read, so unset never means “enforced”. Only the apt family is modelled, so zypper, pacman and apk hosts report zero rows and supported, and options other than signed-by, trusted, enabled and allow-insecure are ignored; a file a writer keeps empty for the whole read looks genuinely empty.

Source and tests