Appearance
File Registration
Concept layer — frozen. The File Registration generic. Nothing here is written by a normal playbook run; a project's own feature analysis is the live document and takes every edit. This layer names no project and links to none — the dependency runs one way, from an application to its concept.
Business Context
A system that accepts files from outside itself — uploads, mailboxes, integrations, scanned documents — accepts whatever the sender chose to send. Some of it is malicious, more of it is simply not what it claims to be, and every downstream process is written on the assumption that neither is true.
File Registration is the gate between the two. It takes custody of an incoming file, decides whether it is acceptable, and produces a durable record of that decision. Nothing downstream reads a file that has not been through it.
Business-Level Definition
Quarantine, inspect, promote. An incoming file is first stored unmodified in a quarantine location. It is then inspected — the true type derived from its contents, not from its name — against what the calling process is prepared to accept. Only a file that passes is promoted to the location where business processes read files. A file that fails stays in quarantine and is never promoted.
The pattern's value comes from the asymmetry: the raw file is kept because it is evidence, and the clean location is trusted because nothing arrives there except by passing.
Requirements Definition
- Ingest every incoming file into a quarantine store, unmodified.
- Determine the file's true type from its contents, and require it to agree with the extension the sender claimed.
- Apply the caller's allow-lists, denying by default.
- Optionally apply a caller-supplied content-level validation for formats the business process must parse.
- Promote a passing file to the clean store; leave a failing file where it is.
- Persist file metadata, and record a registration report capturing what was inspected and what was decided.
Technical Context
What kind of thing this is
An abstract service invoked in code, not an endpoint and not a long-running business process. It is called by whichever process ingests files, and it is deliberately ignorant of what those files mean. Because it is abstract, it prescribes no storage keys, no folder layout and no accepted types — all of those are parameters supplied by the caller.
That ignorance is the design. A registration service that knew about invoices would need changing every time a new kind of document arrived; one that knows only bytes, types and locations does not.
The service works only against file-centric entities and never touches a business entity.
User Stories / Use Cases
- As an ingesting process, I hand a file to the registration service and receive a verdict, without implementing type detection or malware scanning myself.
- As a security reviewer, I can show for any file in the clean store what was checked and when.
- As an operator, I can find every file that was blocked, and why.
UI/UX Design
None. This is an internal service. Its outcomes surface in whatever screens the calling process owns — which is where an end user learns that their upload was rejected, and needs to be told something more useful than "failed".
Functional Requirements
Service parameters, supplied by the caller
| Parameter | Purpose |
|---|---|
| Allowed content types | The types this business process can accept |
| Allowed extensions | Compared against the file's claimed name, lowercased first |
| Quarantine location | Where the untrusted original is read from and stays |
| Clean location | Where a passing file is promoted to |
| File record identifier | The metadata row this run belongs to |
| Optional content validation | A callback for format-specific checks the caller needs |
The decision
A file is accepted only if all of the following hold, and denied otherwise:
- the claimed extension, normalised to lower case, is in the allowed list;
- the type detected from the file's contents is in the allowed list;
- the detected type and the claimed extension agree with each other;
- every inspection the project runs returns a clean verdict;
- any caller-supplied content validation passes.
Everything not explicitly allowed is denied. An allow-list of accepted types is a security control; a deny-list of known-bad ones is a guess about the future.
The extension is a claim; the detected type is evidence. Checking only the extension accepts an executable named
When a run is admissible. A run is admissible from quarantined, and — as an explicit re-inspection — from failed or rejected. A run over an accepted file is a re-inspection under changed rules and must be started by the re-inspection path (see Backward Compatibility and Migration), never by the ordinary ingest trigger. The service refuses runs from any other state. Without the precondition, every trigger that can name a file can re-read it, re-promote it and add a report to it, as often as it likes.
Outcomes
Three, and each is a distinct recorded state:
| Outcome | Meaning | Effect |
|---|---|---|
| Accepted | Every check passed | The original stays in quarantine; a copy is promoted to the clean store |
| Rejected | A check failed — the file is not acceptable | Nothing is promoted; the original stays in quarantine |
| Failed | The inspection could not be completed — storage error, inspection service unreachable, timeout | Nothing is promoted; the original stays in quarantine |
Rejected and failed must not collapse into one status. Rejected is an answer: this file is not allowed, and retrying changes nothing. Failed is the absence of an answer: the file may well be fine, and the run should be retried. A system that records both as "not clean" will either retry rejected files forever or abandon files that only ever hit a transient error — and it can never report how many files it actually blocked.
The project names what retries a failed run — a scheduler, an operator action, or nothing — and if nothing, says so. "Should be retried" with no owner is a file that stays in quarantine until someone notices.
Each outcome writes the registration report and updates the file's status in one transaction, so a file is never left in a state that claims a verdict no report explains, or vice versa.
What the registration report must record
The report is the durable evidence of a decision, and it must be readable years later by someone who does not have the file. The concept requires:
- A digest of the bytes that were actually inspected, over a cryptographic hash. This is what ties the report to a specific sequence of bytes rather than to a name or a row id.
- The size of those bytes.
- The detected content type, and what it was detected from.
- The outcome, and — for a rejection — which check failed. A report that records only "not clean" cannot be acted upon.
- The raw result of each inspection step that was run, one field per step, so that a later question about a specific tool's behaviour can be answered without re-running it.
- The scope key and the standard audit block.
Two constraints on that list, both of which are easy to get wrong:
- The report's fields must match the inspections the architecture actually performs. A report contract that names a tool the design never introduces produces a column that is always empty and a reader who believes a check happened. Add a field when the project adds the inspection, and not before.
- A digest can only attest to the bytes it was taken over. If a project wants to prove that the file did not change between ingestion and inspection — a real concern when the two are separated in time and the quarantine store is writable — then it must record both digests, the one taken at ingestion and the one taken at inspection, and compare them. Comparing two digests when the report has a field for only one is not a check; it is a comparison whose result nothing preserves. And the ingest-time digest is evidence only if the system computed it over the bytes it actually received. A digest declared by the sender is a claim, and may be recorded as one, but the comparison the concept requires is between two digests the system took itself — otherwise a digest the sender chose is compared with bytes the sender chose, a typo in the claim becomes a permanent rejection, and a correct claim proves nothing about the ingest.
Which inspections to run is the project's decision. The concept requires content-derived type detection, and it requires that whatever else is run is recorded. Whether that means one tool or three, and which products they are, depends on the threat model, the file types in play and what the project is willing to operate. Name them in the project's own analysis, together with a field in the report for each.
Internationalization & Localization
Not applicable to the service. The calling process is responsible for turning a rejection into something a person can read, in their language.
Non-Functional Requirements
- Isolation. Inspection runs in an isolated execution environment with no outbound network access and least-privilege access to storage — read from quarantine, write to clean, nothing else. Inspection means parsing hostile input, so the process doing it is the most likely thing in the system to be compromised, and it should be the least useful thing to compromise. For that grant to be possible at all, the quarantine and clean locations must be distinguishable to the storage system's own access control — separate buckets, or prefixes a bucket policy can address. Otherwise "least privilege" cannot be granted and the isolation is a naming convention. A project that runs both in one bucket under one credential says so in its analysis, as a deviation.
- Streaming. Hash and inspect as a stream, including any caller-supplied content validation. Loading whole files into memory turns a large upload into an availability problem, and a format check that buffers the whole object reintroduces it after the hash and the detector avoided it.
- Bounded concurrency, so that a burst of ingestion cannot exhaust the inspection capacity.
- Timeouts on every inspection step. A step that hangs must produce a failed outcome rather than an indefinitely pending file.
Processes & Related Systems / Components
- Object storage, holding the quarantine and clean locations. The concept assumes object storage and names no product.
- An inspection component, isolated as above.
- The calling business processes, which own what happens next.
Related concepts: Multi-Organizations / tenant scoping (both stores and both entities are scope-bound), Audit Log where a project audits registration outcomes.
Diagrams & Models
text
ingest → [ quarantine ] → inspect → accepted → [ clean ] → business process
↑ │
└── original always ──┴── rejected / failed: nothing promotedThe original never leaves quarantine, including on success. Promotion is a copy, not a move — the untouched original is what makes a later investigation possible.
The same lifecycle as a sequence, with the branch that decides whether anything is promoted:
sequenceDiagram
autonumber
participant P as Calling process
participant R as Registration service
participant Q as Quarantine store
participant I as Inspection component
participant C as Clean store
participant D as File record + registration report
P->>R: Register — allow-lists, both locations, the file record, optional content validation
R->>Q: Store the original, unmodified
R->>Q: Read the bytes as a stream
R->>R: Digest the bytes, then detect the type from their content
R->>R: Extension allowed, detected type allowed, and the two in agreement
R->>I: Inspect — isolated, no outbound network, bounded by a timeout
I-->>R: A clean verdict, a not-clean verdict, or no verdict at all
R->>R: Caller-supplied content validation, where one was given
alt Every check passed
R->>C: Promote a copy
R->>D: Report + status = accepted, in one transaction
R-->>P: Accepted
else A check answered: not acceptable
R->>D: Report + status = rejected, naming the check that failed, in one transaction
R-->>P: Rejected — nothing promoted, and retrying changes nothing
else A check could not be completed — unreachable, timed out, a storage error
R->>D: Report + status = failed, in one transaction
R-->>P: Failed — nothing promoted, but the run can be retried
end
Note over Q: The original remains in quarantine on every outcome, acceptance included
Two things the diagram is making explicit rather than decorating. The branch has three arms, not two, because rejected and failed are different answers and only one of them is worth retrying. And in all three the report and the status are written together, so no reader ever finds a verdict with no report behind it.
API Analysis (API-A)
None — deliberately. The service is invoked in code, so what needs documenting is its internal interface: the parameters above, the three outcomes, and the guarantee that the report and the status are written together. A project that exposes registration over HTTP has made its quarantine gate callable by whoever can reach the route, which is a decision to take explicitly rather than by default — and that route must carry the same scope check as every other read by identifier, and the same state precondition as the service itself. A route that takes a bare file identifier, looks it up without the scope, and runs regardless of status lets any permitted caller re-register any file in the system on demand.
Domain Model & Data Attribute Table (DAT)
- A file entity — the metadata record for one file: its location, its claimed name and type, its registration status, the scope key, and the standard audit block. The claimed type is preserved after registration; the detected type is recorded beside it, not written over it. The claim is what the mismatch check was made against, and a record that keeps only the verdict cannot show what the sender said.
- A registration report entity — one run's evidence, as specified above, referencing the file.
Keeping them separate matters: the file has one current status, and it may have been inspected more than once.
A project that lets any component other than the registration service write to the clean store — a trusted derivation such as the members of an archive the service already accepted, or an artifact the system produced itself, such as a preview image — must name each such producer in its analysis and mark the resulting file records so that "promoted by inspection" and "written by a trusted producer" are distinguishable in data: a distinct status value or an origin column, not an actor string. A reader of the file table must be able to tell a file that passed the gate from a file that was born clean.
Indexing Strategy
- The scope key on both entities.
- The registration status on the file entity — the query that matters operationally is "what is sitting in quarantine", and it runs against a table where most rows are clean.
Data
No seed or configuration data. The allow-lists belong to the calling processes, not to the service.
Logging & Monitoring
.info— each phase of each run: ingested, inspected, promoted, and the outcome..debug— detected type and the detail of each inspection step, including the version of whatever performed it. Versions matter here: "was this file inspected before or after we updated the signatures" is a question that gets asked after an incident — and it is asked per file, long after the logs have rotated, so the report, not the log, is where the version belongs. Record it in the raw-result field of the step that produced it..warn/.error— an inspection step unreachable or timing out; a storage operation failing.
Worth monitoring: the rate of failed outcomes, which is an infrastructure signal, separately from the rate of rejected ones, which is a content signal. See the outcome table above for why collapsing them destroys both.
Caching and Performance
None. Every file is inspected; a cached verdict is a verdict about bytes you are choosing not to look at.
Backward Compatibility and Migration
Tightening an allow-list does not retroactively invalidate files already promoted. If that matters — and after a discovered vulnerability it does — the project needs a deliberate re-inspection path, and the report's digest is what makes it possible to say which files were inspected under the old rules.
Legal Context
The mechanism is deliberately ignorant of what a file contains, which is what makes it reusable — and also what makes these questions the project's rather than the concept's.
- How long are refused files kept? Quarantine holds the original on every outcome, rejections included. So a project retains material it declined to accept, in a place its ordinary deletion paths may not reach. That is a decision, and leaving it unmade means "forever".
- What happens to a record that never received bytes? An ingest that reserves a file record before the upload lands leaves a quarantined row with no object behind it whenever the upload is abandoned. Nothing inspects it, nothing promotes it, and no retention rule written for files reaches it. The project says what expires those rows, and after how long.
- Does an erasure request reach quarantine? If uploads can carry personal data — and for most products they can — then a request to erase a person has to find files that were never indexed under that person, because inspection ran before anything was interpreted.
- Is the inspection verdict itself a record worth keeping? The report says what was refused and why. That may be evidence a project wants, or a log of failed submissions it would rather not hold. Both are defensible; they point at different retention rules.
- Does anything constrain where quarantined bytes may live? Data-residency and sub-processor terms usually attach to storage, not to the code that writes it, so this question is answered by the storage choice rather than by the design.
Cybersecurity Considerations
This is a core safeguard, and one of the few features whose entire purpose is security. Its properties, in order of importance:
- Default-deny, with allow-lists supplied per calling process.
- Content-derived typing, not extension trust.
- Isolation of the inspection step, with no outbound network.
- An immutable original, so that a later investigation has evidence.
- A durable, per-run report that says what was checked.
The safeguard degrades quietly: an inspection step that starts failing open, an allow-list that grows to accommodate a customer, or a caller that reads from quarantine "just this once" each remove the guarantee without removing the feature. Changes here deserve review in a way that changes to most features do not.
Risk Assessment
- Bypass. Any process that reads from quarantine, or writes to the clean store directly, defeats the gate entirely — unless the producer is named and its records marked, as the Domain Model requires. The clean store should be writable by nothing but this service and those named producers.
- Failing open. An inspection error treated as a pass. This is why failed is its own outcome and why nothing is promoted on it.
- Detection gaps. No type detection is perfect, and none of it makes a permitted format safe to parse. Downstream parsers still need to be defensive; registration reduces the input space, it does not sanitise it.
- Stale inspection capability. Whatever performs the inspection needs updating, and an isolated, no-internet component does not update itself. The project must say how it does.
Auditing, Reporting & Measurement
- Every registration run leaves a report; the reports are the audit trail of this feature and are never modified after the fact.
- Both entities carry the standard audit and actor metadata, with a system actor for runs the service performs on its own behalf.
- The counts worth reporting: files rejected per period and by failed check (a content signal), and runs that failed technically (an infrastructure signal).
Known instances
None recorded in this copy. Add this project's instance here as it adopts the concept — one line naming the feature that implements it. This is the only place a concept may name a project, and the template ships it empty so a fork inherits no other project's entries.