Skip to content

Guardana Control 0.15.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 17:50
· 47 commits to main since this release
Immutable release. Only release title and notes can be modified.

Upgrading from 0.14 changes four things an operator must act on. collect
writes a directory of segments of one tenant and project and requires a
credential, which the plane sends through export.headers: --dir,
--tenant, --project and --credential-file replace --out; past
--max-bytes the plane's material calls are blocked until an operator prunes
(ADR-0072).
A run belongs to one project: a 1.0 run record is refused, so open each live
run again in a new, empty directory, under a new task
(ADR-0068). A gateway
configuration that classifies a tool WRITE, DELETE or CONFIGURE needs
environment, and an MCP override classified TRANSACT or
IDENTITY_OR_ACCESS needs its upstream's tenant_id. An approval record
whose approver now reads as authn: or mixes scripts resolves as
APPROVAL_STATE_UNKNOWN and is never spent. Formats move too: the evidence
export is 1.1, whose pruned_before the evidence report raises as a gap;
a supervise report that names a source it could not rely on is 0.2; and an
egress import writes observation records 0.3. A 0.14 reader refuses each of
them. New: observe import reads an egress proxy's access log, and coverage
counts an egress path as observed by the connections of the sources it names
(ADR-0069).
Operators can sign a roster of operator keys, though no plane reads it yet
(ADR-0071). The
hook profile's qualification was not run again for this release.

Security

  • The filesystem server the guide's MCP client check installs takes glob
    14.0.2, which fixes GHSA-3g3m-2fjr-6pw4, and minimatch 11.0.1, which
    fixes GHSA-g3hv-75wg-m249, through overrides: the server, at the version
    its fingerprints were taken from, asks for the majors below them, where no
    fix exists. Its search and tree tools ran with them. Nothing this project
    ships uses either.
  • The gateway no longer takes an approver id from its approval store that
    reads as the reserved authn: prefix. Such an answer runs nothing: a retry
    closes the held trail with the plane's own approval, expired, and a
    reconciliation closes a lost hold APPROVAL_STATE_UNKNOWN. Both shipped
    stores already refused such records; a store that did not was believed.
  • The hook profile's qualification harness writes the runtime's stream only
    through a filter that keeps the tool calls, their answers and the run's
    status; the model's text and reasoning never reach the disk, where before
    they sat in the work directory until the scrubbed copy was made.
  • trail export refused a segment of a log named by its path but exported
    one reached through a symbolic link as a whole trail file, so coverage and
    supervise took a log with pruned segments as whole. The file opened is
    now judged by its real directory.
  • The spool's quarantine took a record carrying an event id it already held
    as preserved, without comparing content, so a second record with that id,
    or with no id, was acknowledged and lost. It now tells records apart by
    their whole line.

Added

  • A test holds scripts/ports.txt to the tree: every line whole, its
    interface an exported interface of its package, its conformance cases
    present with a negative control for every rule, its doctor key named, and
    none only where the port's record says the component has no doctor;
    a testdata/ports/ directory, or an interface whose comment calls it a
    port, with no line fails it
    (ADR-0066).

  • The dependency rule refuses, in a non-test file under adapters/,
    importing internal/policy/reasons (check-imports.sh reads every file
    whatever its build constraints; depguard too), and composing or holding a
    v1 Decision by value (a type check; reflection is out of its reach),
    each shown refusing a planted violation by check-imports-probe.sh.

  • A test holds docs/adr/README.md to one row per record.

  • guardana-control operators roster sign signs a roster of operator keys under
    a trust root, each key with grants of one tenant, one project and a level,
    for 24 hours unless told otherwise and at most 7 days, each signing at a
    higher serial. Signers writing into one --out directory hold a lock on it,
    so they never write one serial twice; it needs flock, and without it the
    command refuses. Of two rosters of one serial written elsewhere, the roster
    port's floor takes the first and refuses the second.
    operators floor init starts the floor a plane raises as it takes a
    roster, for a roster id the roster port takes. The roster, operator
    authentication and operator authorization are ports with these as their
    defaults, and a veto may only deny. No plane reads a roster yet
    (ADR-0071).

  • observe import reads an egress proxy's access log. A source of kind
    egress-access-log names which top-level member of each JSON line holds
    the time, host, port, method, status, byte counts and caller, and each line
    becomes one connection observation; no other member is read, so a path,
    query, header or body is never copied unless the mapping names it.
    Fixtures cover Envoy's json_format, as a forward proxy and as a TLS
    listener, and a Squid logformat. The mapping's destination_form reads
    the destination as a host alone or as a proxy request's authority, host,
    host:port, [v6] or [v6]:port, whose port, when it names one, must
    agree with a mapped port; any other destination is refused, and each
    refused line is counted under its reason in the import report's
    refused_by. Under an address basis the caller must
    be an IP address, or it is dropped and counted in callers_dropped. Hosts
    and addresses are written in one spelling, an IPv4-mapped IPv6 address as
    its IPv4 address. Such a source may declare TRUST_INDEPENDENT on an unsigned
    descriptor, and names no run
    (ADR-0069).

  • Source descriptor schema 0.2, which carries the egress mapping; a 0.1
    descriptor reads as before, with the same digest. Which members and which
    trust a descriptor may carry is now decided by its kind
    (ADR-0069).

  • Observation record schema 0.3, for a connection, for an import report
    with refusals by reason or callers dropped, and for a record naming its
    source's kind, which every egress record does. A source's liveness and
    doubt are read from reports of its kind alone, and observe import refuses,
    with exit 2, a write of another kind into a log holding the source's
    records. A connection's id is derived from its content, so
    importing a grown or rotated log again writes only what is new
    (ADR-0069).

  • coverage reads an egress path as observed by the connections of the
    sources it names, ends every egress line with no plane decides egress,
    prints independent trust as independent (unsigned descriptor), and adds
    a row for each host an egress source reached that no path declares, at most
    256 with a line counting the rest, then a row for each egress source whose
    import refused a line, with the count by reason, then a row for each egress
    source given that is not live, since its connections since its last import
    are unseen. Any such row, or that line, makes the exit 1. An absent egress
    descriptor whose log holds egress records is refused, with exit 2, and so is
    a log whose egress records no egress descriptor given with that log names:
    one replaced by another kind or renamed, or another source's log sharing
    the directory (ADR-0069).

  • pkg/contract.ValidateUnclassified: every rule of Validate but the
    effect class's presence and what a class requires, for an envelope nothing
    classified. Validate does not change (ADR-0067).

Changed

  • Breaking: trail <file> refuses a file in a log's directory, named
    directly or through a symbolic link, exit 1: name the directory instead.

  • trail of a directory ends its counts line with segments pruned <n>.

  • Breaking: a supervise report names each --source it could not rely
    on in sources_in_doubt: one not read by its state alone, one never heard
    or silent by its id and heartbeat. A report that names one is schema 0.2
    whatever its procedure's schema, so the findings log and findings export
    now say what the exit status said. An earlier release refuses a log holding
    such a line as damaged at that line, and misreads nothing. supervise
    refuses more than 32 sources before reading any file.

  • Both approval stores decide whether an approval may be spent in one order
    (approvals). An expired
    record marked multi-use in an approvals directory is left pending and
    counted until the hold's own expiry, as an unexpired one already was; it
    used to close the held call DENY APPROVAL_EXPIRED. Either way nothing
    runs. A record refused for its approver id blocks only the request it
    names: a lookup lists it as unknown, beside every other record of its
    action, and hides it once it expires; a retry tries its held approval once,
    however many records name its request; a plane closing a lost hold
    resolves only the approval it minted. The memory store refuses a hold that
    is not a pending, unanswered approval with an approval id, a requested time
    and an expiry.

  • Breaking: guardana-gateway collect writes a directory of trail
    segments bound to one tenant and project, and requires a credential:
    collect --listen <addr> --dir <dir> --tenant <id> --project <id> --credential-file <file>, and --out is gone. A request without
    Authorization: Bearer and that credential is answered 401 before its body
    is read; a plane sends it through export.headers. An append of another
    tenant's or project's event is refused 400, which the plane quarantines. A
    segment is sealed at --segment-bytes; past --max-bytes every append is
    answered 503, so the plane's spool fills and its material calls are
    blocked until an operator prunes. Nothing is pruned on its own.
    guardana-gateway prune --through <n> <dir> prunes sealed segments while no
    collector holds the directory, trail segments <dir> prints the manifest,
    and trail <dir> and trail export <dir> read the segments. The directory
    is refused with any group or other access. Until the collector listens on a
    unix socket, an account that binds its loopback port while it is down
    receives the plane's retries and the credential among them
    (ADR-0072).

  • Breaking: the evidence export is 1.1: its header's pruned_before counts the
    segments of a log before the exported one that were pruned, 0 for a single
    file, and coverage refuses an export as whole while it is above 0. A 1.0
    export reads as before; a header of a later minor without the member is
    refused. A strict 1.0 reader, as coverage and examples/evidence-report
    of 0.14.0 are, refuses a 1.1 export on its new member: it fails loudly
    and never misreads
    (ADR-0072).

  • trail export refuses a file inside a directory of segments, naming the
    directory to export: exported on its own it would count none of the
    segments pruned before it.

  • examples/evidence-report raises evidence_gap noted pruned, and exits
    1, for an export that names no cursor and counts segments pruned before it;
    an export after a state's cursor raises none for them.

  • doctor no longer changes the evidence spool it checks. It reads the spool
    through spool.Inspect, which cuts nothing: a torn tail stays in place, and
    the line says "a start would cut a torn tail of N byte(s), which this check
    left in place"; a torn tail whose file its owner may not write fails the
    check, as it fails a start. It asks access(2) whether the directory takes a
    file instead of writing a probe, and no longer prints a reserved count.
    Every command is marked as one that inspects or one that writes, and each
    that inspects is tested to change nothing
    (ADR-0016).

  • Breaking: a run belongs to one project. A run record is 2.0 and
    carries project_id, runs open needs --project and refuses a parent of
    another project, and runs list prints project_id. A plane resolves a
    token only under its own project_id, so a token of another project's run
    gets 401 even on a shared runs directory and listener identity. A run
    tree refuses a member of another project, supervise refuses evidence of
    another project than the run's record, react never stops a run for a
    finding of another project, and doctor lists a stop of another project's
    run as unheld. A 1.0 record names no project and is refused: open each
    live run again in a new, empty directory, under a new task, since a
    reopened run starts with a clean flow state
    (runs.md,
    ADR-0068).

  • The coverage inventory refuses an egress host, or an egress path's
    source name, that is not a host in the one spelling a connection's host
    is written in, and names that spelling: https://x/ and a non-ASCII letter
    are refused, and so are Example.COM, written example.com, and
    ::ffff:203.0.113.7, written 203.0.113.7. It refuses
    server_address on an egress path's source. Neither could match before
    (ADR-0069).

  • supervise refuses, with exit 2, a --source of a kind whose
    observations name no run, such as an egress access log, and one with no
    kind (ADR-0069).

  • observe import refuses an input the group or others can write when the
    descriptor declares TRUST_INDEPENDENT
    (ADR-0069).

  • A log that holds a 0.3 line cannot be read by 0.14: its observe import
    and coverage refuse the log as damaged, and its observe export counts
    each such line as an unsupported_version gap
    (ADR-0069).

  • Under OBSERVE, a call the adapter could not translate, for anything but
    nothing classifying it, is now blocked with the kernel's INDETERMINATE
    and the refusal's code, on the trail ENFORCE writes, and counts as
    blocked. An MCP answer carries that code in place of MALFORMED_INPUT, and
    an enforcement API BLOCK no longer appends INVALID_FIELD_VALUE
    (ADR-0067).

  • The gateway refuses, in every mode, an envelope whose action kind is not
    tool, prompt or resource, or whose protocol is not its adapter's, as
    INVALID_FIELD_VALUE. The kinds, the protocol names and each protocol's
    enforcement strength are one list in internal/channel (ADR-0067).
    It also refuses a resource pointer an adapter hands in that is not RFC 6901,
    and at start an adapter whose name is not one of those protocols, or whose
    declared enforcement is not its protocol's.

  • A rewrite that moves the member a tool's resource_from reads is now a DENY
    of the gateway's own, OBLIGATION_NOT_UNDERSTOOD, before anything is handed
    out, for both adapters, in place of an aborted execution that ended
    ACTION_FAILED (ADR-0067).

  • coverage and the run view read a plane's strength from its protocol: a
    trail that names no protocol, or one this build does not know, is decided,
    not enforced, in every mode (ADR-0067).

  • Breaking: an approver id that a reader could take for one the plane
    authenticated is refused: approvals approve and reject exit 1 and write
    nothing, console exits 2, a scenario that names one is refused when it
    loads, and
    a record that holds one resumes nothing and its reconciliation closes as
    APPROVAL_STATE_UNKNOWN. Refused are, in order, an id that begins with
    authn: in any ASCII case once leading default-ignorable code points are
    skipped; one holding a private-use, unassigned or noncharacter code point,
    a default-ignorable one, or a blank symbol such as U+2800; one whose
    letters mix scripts beyond UTS #39's highly restrictive level (one script,
    or Latin with Han and Hiragana and Katakana, with Han and Bopomofo, or with
    Han and Hangul); one that puts a combining mark on ASCII punctuation or a
    symbol; one holding a fullwidth, mathematical, enclosed, letterlike, small,
    vertical, superscript or subscript form; and one whose first six visible
    characters read as authn: with an ASCII letter among them, where any
    character outside ASCII matches any position. So Владимир, 山田太郎さん
    and zoë are accepted, while аuthn:x with a Cyrillic а, autℏn:x,
    àuthn:x and authn∶x are refused. The cost is that a name written in
    fullwidth, mathematical, enclosed or halfwidth letters is refused, and so
    is an id that begins like authné:. A lookalike written wholly in one
    script other than Latin, with no ASCII letter among its first six
    characters, is accepted, such as Lisu ꓮꓴꓔꓧꓠ:ꓧꓠ. The plane never writes such an
    id, and a machine comparing bytes never takes it for the prefix. Only the
    plane will write that prefix, for an approver it authenticated
    (ADR-0066).

  • Breaking: an approval record written by 0.14 whose approver the rule above
    now refuses, one that mixes scripts, holds a code point or a form now
    refused, or reads as authn:, resolves only as APPROVAL_STATE_UNKNOWN
    and is never spent. It refuses its own approval and no other: lookups,
    holds and prunes of every other record go on, and it is pruned once it
    expires.

  • A gateway configuration that classifies a tool WRITE, DELETE or
    CONFIGURE is refused when it loads unless environment is set and, for an
    MCP override, its upstream's environment too; an MCP override classified
    TRANSACT or IDENTITY_OR_ACCESS is refused unless its upstream's
    tenant_id is set. mcp.New and enforce.New refuse the same: the
    contract needs these on such a call's envelope, and every call to the tool
    was refused, OBSERVE included.

  • The roadmap starts milestone 2 with the sensors independent of the agent
    (B21), then the port contracts and the TypeScript client (B12), and lists
    the open items of the 1.0 release bar as backlog tasks B29 to B34
    (ADR-0064).

  • A record now decides that each decision about identity, authentication,
    authorization, approval, the policy language, evidence, observation,
    detection, reaction, the signature algorithm and keys sits behind a port.
    Each port has a tested default that an operator may replace, in this
    repository or through one remote kind, and each fails closed. Nothing a
    plane does changes with it yet
    (ADR-0066).

  • A pull request's gate no longer fuzzes: make quality runs the seeds of
    every fuzz target in its tests and checks with make fuzz-list that each
    target compiles, while the nightly run and the release commit's cold gate
    fuzz each one for ten seconds. docs-check leaves make quality, whose
    tests run the same packages
    (ADR-0062).

  • make test and make test-race fail on a skipped test that
    scripts/expected-skips.txt does not name, or one whose skip message does
    not say what the list says; the sixteen tests that skip on Linux or macOS
    are named there
    (ADR-0063).

  • make lint lints the generator programs under scripts/ too, one at a
    time, which no lint read before.

Fixed

  • policy test skipped a case named *.json that is a pipe or a socket and
    passed on the rest; it now refuses the directory, naming the entry.
  • dev ended with status 0 after its console exited with a failure; it now
    reports the console's failure.
  • scan-binaries.sh passed over a file whose first bytes it could not read,
    which could be a Go binary left unscanned; it now refuses it.
  • fuzz-list and fuzz-smoke missed a fuzz target that a build constraint
    leaves out when a comment stood between func and its name; every
    declaration gofmt can leave is now read.
  • The qualification harness passed block when the runtime was stopped
    before it answered the refused call; such a run is now unknown.
  • A collector whose live segment changed under it refused that append but
    went on to seal it, writing a manifest its next start refused. The log now
    takes nothing more until it is started again, and that holds too when the
    first append after the change needs a seal or repeats events it holds.
  • prune with the clock before a segment's seal wrote a tombstone the
    manifest reader refuses, leaving the directory unreadable. It is now refused
    with nothing changed.
  • An export or a read of a log between a seal's manifest and its rename took
    the sealed bytes as the next live segment; it now refuses them as moved and
    starts again, and fails once the stopped seal outlasts its retries.
  • After Prune on an open log, an event whose line was pruned and sent again
    was taken as written; the index now covers only the segments left.
  • evidence-report -state refused its own state after a request stayed open
    across a segment, and raised again an alert of the next segment logged
    before a crash. After a crash it could also take the next segment's gap
    after its last newline for an earlier segment's at the same offset, and
    not raise it; an alert with no cursor now names its file as source.
  • A deadline finding is CONFIRMED when one evidence export holds both events
    it cites, whatever other exports were given; it was SUSPECTED whenever
    more than one export was, even the same export twice. Endpoints no one
    export holds stay SUSPECTED, since two planes' clocks differ.
  • The collector answers 503, not 400, to a body it could not read to its end,
    as one cut by a reset connection or a read timeout, so the plane sends it
    again instead of quarantining it.
  • tools/list under hide left out a tool whose only ALLOW rule reads
    the resource id its classification points at, and annotate marked it
    VERDICT_DENY, though a call naming an allowed id ran. A tool previewed as
    the DENY of no matching rule alone now stays listed, marked
    DECIDED_PER_CALL, when an ALLOW rule lacked nothing but that resource id
    or the run's flow. An ALLOW unknown on anything else, a matched DENY
    rule, LOCKDOWN and a halt still leave it out. A tool whose preview is not
    a verdict stays listed, as before: one that needs a resource id, whose
    preview the contract refuses, and one an undetermined rule decides
    (ADR-0070).
  • OBSERVE handed out a call whose refusal joined "nothing classifies it"
    with another cause, such as arguments canon refused: such a refusal read as
    unclassified alone. Unclassified is now that one cause, on a chain of single
    wraps (ADR-0067).
  • OBSERVE handed out a call whose envelope the contract refuses when the
    adapter reported no refusal; the kernel's refusal of the request now blocks
    it whatever the adapter said (ADR-0067).
  • OBSERVE handed out a call the adapter said only nothing classifies, though
    its envelope set an effect class, broke a rule of the contract, such as a
    missing principal, a string past its bound or an unanchored delegation
    chain, or carried a hash of other bytes: the kernel stops at a refusal
    handed in and checked none of it. The gateway now holds every such claim to
    ValidateUnclassified and the arguments hash and blocks the call with the
    kernel's code for what is wrong (ADR-0067).
  • A pause entry scoped to an action kind let a call through whose kind was
    spelled outside the plane's list, such as Tool or function; such a kind
    now matches every action scope, as an absent one does
    (ADR-0067).
  • A trail whose events named two runs passed the chain check, so
    guardana-gateway trail and the evidence report read it as one request
    completed under its first run. Every event of a request now names the run
    its first event names, or none for an action outside a run
    (ADR-0065): trail
    reports such a trail failed at the event that changes the run, and the
    evidence report reports it unknown, in its follow mode too, whose state
    moves to schema 1.2 to keep each ended request's run; a 1.1 state still
    loads. A run_id longer than the contract's 1024-byte string bound breaks
    the chain too, as the other identifiers do.
  • guardana-control supervise checked the rules of a request whose later
    events named a run outside the ones it read, or none, as if the request
    were whole. Such a request is now in doubt, and its rules are not checked.
  • The evidence report read an export filtered by run or by event kind as
    whole, so a request's event the filter dropped went unseen. Such an export
    now exits 1 as cut, with the reason, as follow mode already refused it; an
    export filtered by request alone still reads.
  • A hold journal write that failed after its entry was linked could leave the
    entry behind a trail the plane closed without it; the next start closed the
    trail again, forking its chain. The entry now stays, and such a hold is
    closed only once its entry has left held; when it cannot leave, the trail
    stays open for the next start to close from the entry.
  • The hook profile's qualification passed four cases on evidence it did not
    have: two approved retries with different inputs, a project it could not
    list, and hooks disabled or MCP tools left out with no positive control in
    the run. Each now needs its evidence or is unknown, a results file counts
    only where its own controls passed, and the harness has 74 tests run by
    make check-python. Judged again, the 2.1.310 run passes 19 of 20 cases:
    disabled had no control invocation.
  • Statements on seventeen public pages that the code contradicted are
    corrected: among them an identical retry after a spent approval, which is
    refused until the approval expires, how many times expiry is judged, three
    answers of the enforcement API, the stateless listener's older revisions,
    supervision's bounds and ordering, and the evidence report's exit 2.
  • scripts/publish-python-dist.sh compared the release files' digests with
    the build's before uploading, but a failing sha256sum gave an empty digest
    on both sides; each digest is now checked to be 64 hex digits.
  • The uv version check every Python gate script calls took uv 0.12.9-beta.1
    for a pin of 0.12.9; it now compares uv's whole version word.
  • A workflow step that wrote SHELLOPTS=noexec or BASH_ENV to
    $GITHUB_ENV could make a later step's bash, the quality gate's among them,
    run nothing and succeed. Every workflow now starts its steps in a bash that
    ignores the shell's startup files, options and functions from the
    environment; a script a step starts still reads them, and make quality
    refuses them.
  • make quality MAKEOVERRIDES= SHELL=/usr/bin/true ran no check and exited 0:
    the guard read the command line from MAKEOVERRIDES, which the command line
    can empty. It now refuses every command-line variable but GO by its origin.

Image: ghcr.io/guardana/control-gateway@sha256:ec566a84b63268d76e388a35552a6c98835f2fd9c890c18f6d0b61d8cbb8980d