KoniKodeKnowledge BaseMy AccountOpen website

KNOWLEDGE BASE

Documentation

DOCUMENTATION
CURRENT APPLICATIONClean Framework
DocumentationClean Framework

Clean Framework · DOCUMENTATION

File audit search

File audit search

Bounded candidate reference — 2026-10-20: 0.2.0-rc.1 is committed locally through 84f415b3098af5653577b2c65eea31fca21df79c, unpublished and not production-approved; stable PHP >=8.5.11 <8.6.0 is required. This is documentation, not adoption: this knowledge base retains its installed 0.1.0-rc.1 dependency and PHP 8.2.11 local profile. Dated PHP 8.2 evidence is historical; earlier dirty-source reports retain their original test provenance, not retroactive clean-commit proof. Candidate examples are illustrative: API signatures were reviewed, but candidate code was not executed by this editorial update. See current checkpoint, open criteria and adoption boundary.

FileAuditSink searches one application-selected, private, single-host newline-JSON audit file. It does not discover databases, consumer directories, other logs or provider histories. Applications explicitly record the events and authorize callers before exposing any search interface.

Result limits and work limits are different

BoundaryMaximum
Matching resultsAuditQuery::limit(): default 100, maximum 1,000
One batch100,000 physical records examined or 64 MiB read
Complete search1,000,000 physical records examined or 640 MiB read
One accepted encoded record, including newline32 KiB

MiB means 1,048,576 bytes. The total byte ceiling is 671,088,640 bytes. Record sizes vary; a batch is not a promise that 100,000 records equal 64 MiB, nor is a million-record search necessarily ten batches.

The search uses bounded read-ahead, counted against both byte budgets. The physical-record counter counts every encountered record submitted to the existing decoder, not matches. Unexamined read-ahead is not decoded beyond the record ceiling. Memory holds a small read buffer and bounded matching results, not the entire 640 MiB history. This is still one synchronous call, not a background job or a new public pagination/cursor API.

Completion and safe failure

A call succeeds when it reaches the query's result limit or the end of its starting history. Exact completion on a work boundary succeeds. If additional scanning is needed beyond either total ceiling, it throws RuntimeException with the fixed message Audit search scan limit exceeded. The accumulated matches are not returned as a successful partial result. Never translate that failure into an empty history or a completed compliance/export response.

Existing filters remain inclusive date range plus exact actor, action, target ID, correlation ID, operation ID and environment. Records retain their encounter order and accepted additive fields. A successful early result limit does not attest to later, unexamined records. Encountered malformed JSON, invalid fields, oversized records and incomplete framing still throw the existing value-free integrity exception. No history is repaired or rewritten.

// Application composition selects an existing private audit location.
// A controller/use case must authorize audit access before calling the sink.
$audit = new \Clean\Framework\Audit\FileAuditSink($privateAuditPath);
$query = new \Clean\Framework\Audit\AuditQuery(
    actor: 'example-actor',
    action: 'document.updated',
    limit: 100
);

// Only a normal return is a successful bounded query. Let the application's
// safe error boundary handle failures; never use catch-to-empty-array here.
$events = $audit->search($query);

Missing files and missing read parents retain the original [] behavior, without creating history. A valid empty file and a file deleted after prior writes also return []; the adapter has no trusted initialization anchor. This compatible default was explicitly approved in DEC-B-02. Do not treat an empty response as proof that history never existed or was not lost. An opt-in strict initialized-history mode remains unimplemented and is tracked for a separate consumer-compatibility/security review as CF-B-F01.

Writers, boundary and filesystem assumptions

The first shared lock captures the file's starting length. Between batches, the reader releases that lock and cooperatively yields before reacquiring it. It retains its open stream, raw-read position and unconsumed buffer. Existing records are neither restarted nor skipped, and later appends are excluded. An observed truncation below the starting boundary fails closed on resumption.

The file adapter now enforces the owner-approved strict private profile below, including file/parent identity checks before admission and after batch lock reacquisition. It still relies on cooperating append-only writers. It is not an immutable snapshot against same-UID rewriting, truncate-and-regrow, a universal atomic-nofollow guarantee, or proof on an unqualified host.

Releasing a batch lock gives waiting writers an opportunity; it does not guarantee fairness or a maximum wall-clock/lock-acquisition time. Choose an application execution context and request timeout appropriate to its history; larger-history adapters require separate design rather than silent fallback. This adds no rotation, cryptographic chain, fsync/power-loss guarantee, distributed locking, or file-plus-SQL transaction atomicity.

Strict private file-audit profile

This policy applies only to FileAuditSink, not every framework logger or framework-wide OS support. The initial qualified adapter profile is native Linux with existing effective-user identity support. It refuses operation when that identity cannot be established; no extension is installed on demand. Ordinary logging and future shared-group, ACL, database or other-platform adapters are separate concerns, not automatically enabled or qualified here.

  • The configured filename must be an absolute native path, at most 4,095 bytes, without control characters, empty/dot/traversal components, stream wrappers, or symlink aliases in any existing component.
  • The immediate parent is owned by the non-root effective PHP user and has exactly 0700 permissions. Ancestors are root- or process-owned and not group/world writable. Root-owned sticky /tmp is permitted above, never instead of, the application's private parent.
  • An existing audit file must be regular, have one hard link, belong to the effective PHP user, and have exactly 0600 permissions.
  • Unsafe existing files/directories are refused, not silently chmodded. Only directories/files created by that call receive checked private modes. No existing audit bytes are migrated, truncated, rotated or repaired.
  • A Linux directory descriptor coordinates admission/initialization with cooperating users of this adapter. It is released after the file lock and validation, not retained for the entire search. This prevents cooperating first writers from seeing the temporary new-file permission state without changing process-wide umask or worker-concurrency settings.

The single owner is an OS service account, not an authenticated person's application identity. CLI jobs and PHP-FPM workers accessing the same audit file need that approved account. Different-UID deployments and pre-existing 0644 files may now refuse even if they worked before. Operator preparation requires separate application/host authority; this framework work did not modify any consumer or real log. Symlink-based deployment layouts must supply a deliberately chosen canonical private location, not an automatically resolved alias. The adapter cannot infer Apache public roots or aliases: keeping audit storage outside every public mapping remains deployment policy.

Observed unsafe type, ownership, mode, identity or permission-operation failure produces RuntimeException with the fixed message Audit filesystem policy check failed. No raw path, event, UID or warning is included or chained. Existing-file streams open without create/truncate; exclusive-lock tail validation positions the stream at EOF before append. Partial writes still fail and retain damaged bytes for separately authorized operator handling. A failed new-file initialization may leave an empty file; it is not automatically removed, repaired or acknowledged as an audit event.

Pre/post-open checks cannot prevent every intervening path change by a privileged or same-UID attacker. Such attackers and hostile/unqualified filesystems are outside this profile. Controlled observed swaps are tested, but that does not establish universal atomic nofollow or whole-history trust. See the approved profile.

Local proof and scope

The owner-approved contract is recorded in DEC-A-01. The tests use only finite synthetic histories in the approved disposable PHP 8.5.11 fixture. audit-search covers million-record work, filters, framing, corruption and actual waiting-writer progress. audit-bytes alone uses the approved 768 MiB scratch mount for a real history over 640 MiB. Original uninstrumented integrity tests also run. These are local regression results, not independent security review, consumer or deployment qualification. Current predicate reconciliation and the strengthened two-writer proof are in the A-010 evidence addendum. audit-filesystem covers private creation and refusal, real native FIFO/socket/ link checks, controlled observed replacements, actual non-root permission denial and the real foreign-UID component guard. Its chmod-false control is explicitly injected, not reported as a real OS chmod errno. No root process or new identity/mount is introduced. audit-scale measures fixed 1k/10k/100k datasets with actual owned writers; timings are observations, not an SLO. The separately approved OPS quota exercises actual kernel ENOSPC and retained partial-tail refusal, not a simulated disk-full result.

The current Phase A outcomes, exact qualification runs and limits are in the A-020 report. Earlier checkpoints remain historical evidence, not the current completion status.

Editorial provenance

Source: docs/AUDIT_SEARCH.md. Source SHA-256: b46511e4282fb1410372aa524b5982221f8b2693ece2bf8738fcf8df48314f1b.

Framework candidate reference; editorial checkpoint 2026-10-20. See candidate status and adoption boundary.

EXPLORE FURTHER

Drag a divider or use Left and Right Arrow keys to resize. Hold Shift for larger steps. Enter hides or restores the sidebar; Home and End select its minimum or maximum width. Double-click resets the width.