All features

Feature breakdown · built with CraftHQ

FilesSecurity

File upload with virus scanning

Whole-team time at this build's pace

Estimated ~20 h

2 people incl. the product owner, requirements to merge

Estimate at this build's pace, not a measurement of this feature

Every upload is encrypted at rest, versioned, and admitted only if its content matches an allowed type; files live in a per-customer store and are served only as authenticated downloads. A virus-scanning step is built and tested and switched on per deployment; when enabled, every upload is scanned, a file reported infected is blocked on download and audited, and an upload is refused if the scanner cannot be reached.

Size: 25 FP · In-house IFPUG-style count, cross-checked by a second counter; not certified. Range 18 to 30. · How we counted 25 FP

How long a feature this size usually takes

Whole-team time — this build vs a typical team

25 FP · FP × 6.8 to 9.4 h (ISBSG 2023)

Whole-team time at this build's pace2 people incl. the product owner, requirements to merge
~20 h15 to 24 h

Estimate at this build's pace, not a measurement of this feature

A typical team, whole lifecycleRequirements to release · middle half of 512 comparable projects (Java, Agile), ISBSG 2023
~170 to 235 habout 4.5–6 person-weeksmedian ~198 h
grey band = a typical team, middle halfhatched = our whole-team estimate

Both figures are whole-team time over the whole lifecycle, so they compare like for like. Ours is this build's pace — about 0.8 hours per function point across the whole build (two people, including the product owner, with requirements and review time). The industry range is an ISBSG project-level figure applied to one feature, so it is indicative.

Workflow

How it moved through the workflow

Select a stage. The build shipped, so every stage is complete; a person approved the merge.

Request

Files come in from outside the organisation. Each one must be private to its owner, kept as versions, and admitted only if it is a safe, expected type.

Questions

One design decision was recorded before building: scan before storing, or store first and gate the download behind the scan verdict.

Approved

The product owner signs off the breakdown by moving the story into the approved state, and the build runs against that reference.

Definition of ready

The questions are answered.
The acceptance criteria are testable.
The size is estimated.
The scope is checked for splitting.
The product owner has signed off.

Building

CraftHQ's build agents write the code and the tests together, run as a loop; a failing check halts the work until it passes.

Checks

Three checks run on every pull request: one on the product owner's machine as CraftHQ builds, one on the engineer's machine before review, and a code-quality scan in CI. Merging is gated by the CI check passing and a person's approval, enforced by branch protection.

Review

An engineer reviews the pull request and merges it.

Merged

The pull request is merged to main after a person approves it. Nothing merges without that approval.

Definition of done

Every acceptance criterion is checked; where a scenario has a dedicated test it is cited, and where it was verified by review that is stated.
The three checks are green.
A person approved the merge.

Breakdown

The feature breakdown

Business first, then the engineering view. Tab through it.

Overview

The gap it closes

Files arrive from outside the organisation, so every one has to be kept private, kept versioned, and admitted only if it is a safe, expected type.

Who uses it

Customer users supply files into their own workspace; staff attach and verify files on their side.

User story

As a customer, I want to upload my files securely so that only my team sees them and only safe, expected file types reach the platform.

Examples

Example 1

A customer drags a file in. Its size and content type are checked, it is stored encrypted as a new version in the customer's own container, and it is served back only as an authenticated download.

Example 2

A file reported infected is blocked on download and the detection is recorded in the audit trail.

Dependencies & risks

Depends on

A versioned, encrypted file store and a self-hosted virus-scan service.

Risk & mitigation

The first line of defence is admission: a strict content-type allowlist with a magic-byte check, so archives, executables, HTML and SVG never reach the platform. The virus scanner then scans every upload, and a file reported infected is blocked on download.

How it works

Simplified diagrams of the parts involved, the steps of one request, and where it runs.

Architecture

People
Customeruploads a file
Your app
Upload pathtype, size, name checks
File storeencrypted, per-customer, versioned
Scan gaterecords the scan verdict
Virus scannerself-hosted service
Audit logupload, detection and download events
  • 1upload
  • 2admit, then store encrypted
  • 3scan the stored file
  • 4verdict (response)
  • 5record the outcome
Simplified.

Sequence

Browserthe customer
App serverupload and gate
File storeencrypted, per-customer, versioned
Virus scannerself-hosted service
  • 1upload a file
  • 2admit: content type by bytes, 50 MB cap, sanitise the name
  • 3store encrypted as a new version
  • 4scan the stored file
  • 5scan verdict
  • 6record the verdict
  • 7served as an authenticated download; an infected version is blocked
Simplified.

Infrastructure

Client (untrusted)
Web appthe upload screen, in the browser
Your app
App serverupload path and scan gate; authenticates, pins the tenant
Databaseper-tenant isolation, scan state
File storageencrypted, per-customer container, versioned
Virus scannerself-hosted service
  • 1upload (authenticate + tenant check at the edge)
  • 2admit, then store encrypted
  • 3scan the stored file
  • 4record scan state and audit
Simplified.

Acceptance criteria

Each scenario is written as a Gherkin spec. The Engineering tab shows the test that covers each one, or notes where it was verified by review.

@files @security
Feature: File upload with virus scanning
As a customer
I want to upload my files securely
So that only safe, expected file types reach the platform and only my team sees them
Every upload is admitted only if its content matches an allowed type, then encrypted at rest and version-tracked. A virus-scanning step is built and tested and switched on per deployment; when enabled, every upload is scanned and a file reported infected is blocked on download.
Background:
Given a customer is signed in to their own workspace
And an upload slot admits a set of allowed file types with a 50 MB size cap
Rule: A file is admitted by its content, not its name or extension
Scenario: A file whose bytes do not match its claimed type is refused
Given a customer uploads a file whose content does not match its declared type
When the upload path checks the first bytes against the declared type
Then the file is refused before it is stored
Scenario Outline: Archives, executables and active-content files are refused at the allowlist
Given a customer uploads a "<kind>"
When the upload path checks the type against the allowlist
Then the file is refused outright and never stored or expanded
Examples:
| kind |
| zip or rar archive |
| executable |
| HTML or SVG file |
Scenario: An oversize file is refused before it is stored
Given a customer uploads a file larger than 50 MB
When the size is checked from the declared length and again as the bytes stream
Then the file is refused
Scenario: A file name is sanitised and never used as the storage key
Given a customer uploads a file with an unsafe name
When the file is stored
Then the name is sanitised for display and the storage key is system-generated
Rule: Files are encrypted at rest in a per-customer store and version-tracked
Scenario: An upload is encrypted and isolated per customer
Given a customer uploads an allowed file
When it is stored
Then only ciphertext is written, in that customer's own storage container
Scenario: Re-uploading keeps the earlier version
Given a file has already been uploaded to a slot
When a newer file is uploaded to the same slot
Then the newer file is stored as a new version and the earlier version is retained
Rule: The virus-scanning step is built and tested and switched on per deployment
Scenario: With the scanning step enabled, a clean file is scanned and usable
Given the virus-scanning step is enabled
And a customer uploads an allowed file
When the scanner reports the file is clean
Then the file is recorded clean and can be used
Scenario: With the scanning step enabled, an infected file is blocked
Given the virus-scanning step is enabled
When the scanner reports an uploaded file infected
Then every download of that version is blocked and the detection is written to the audit trail
Scenario: With the scanning step enabled, an unreachable scanner refuses the upload
Given the virus-scanning step is enabled but the scanner cannot be reached
When a customer uploads a file
Then the upload is refused and the attempt is recorded in the audit trail
Rule: Downloads are served privately
Scenario: A download is served as an attachment
Given a stored file is requested
When it is served
Then it is sent as an attachment over an authenticated session, never rendered inline and never from a public URL
Scenario: Download is scoped to the owner's organisation
Given a file belongs to one organisation
When a user from another organisation requests it
Then the download is refused
@edge-cases
Scenario Outline: Admission and scan outcomes
Given a customer uploads a "<type>" file
When the upload and any enabled scan complete
Then the result is "<result>"
Examples:
| type | result |
| allowed, scanner clean | scanned, recorded clean, usable |
| allowed, scanner infected | blocked on download |
| archive or executable | refused at the allowlist |
| content does not match type | refused before storing |

Questions

Questions raised in planning, each with the decision taken.

Should a file be scanned before it is stored, or stored first and then scanned?

Scanning before storing blocks the write on the scanner; storing first keeps uploads responsive, with the scan verdict gating the download.

AStore first, then scan. Every upload is scanned, an infected verdict blocks the download, and an upload is refused if the scanner cannot be reached. · recommended
BScan before storing, so nothing is written until it is proven clean.
Decided: Post-store scan (option A). Admission (type, size, content sniff) runs in every build.

How we sized it

A function point measures what the software does — its inputs, outputs, lookups and the data it stores — regardless of language or how many lines it took. Each counted item is weighted by complexity: a user input counts 3–6 points, an output or report 4–7, a lookup 3–6, data the feature owns 7–10, and data read from elsewhere 5–7.

  • The document store this feature owns
    Data the feature owns
    10
  • Upload a file (encrypted, versioned, sent to scan)
    User input
    6
  • Download a file, scoped to who may see it
    Lookup
    4
  • Record and show the virus-scan verdict
    Output or report
    5
  • Total
    25
25 function points (range 18–30). IFPUG-style count: each user action, lookup, output and stored data set, weighted by complexity. Technical plumbing is excluded, and a data store is counted once in the feature that owns it.

Components touched

Upload path

Accepts a file into an upload slot, enforces a single 50 MB cap (from the declared length and again as the bytes stream), sanitises the file name, and admits the file only if a magic-byte check of its first bytes backs the declared type. Archives, executables, HTML and SVG are not on the allowlist and are refused outright.

Encrypted file store

Keeps files envelope-encrypted at rest (AES-256-GCM, a per-object data key wrapped by a master key); only ciphertext is written. Each customer has their own container; a key that is not customer-scoped is rejected. Versions are tracked, so a re-upload keeps the earlier copy.

Virus-scan adapter

Speaks to a self-hosted virus-scan service over its network protocol.

Scan gate

Scans every upload; a file reported infected is blocked on every download of that version and the detection is audited, and an upload is refused if the scanner cannot be reached.

Download handler

Serves a file as an attachment over an authenticated session, scoped to the owning organisation, never inline and never from a public URL.

Data touched

File storewrite

Envelope-encrypted (AES-256-GCM), versioned file contents in a per-customer container; keys are held by the platform.

Scan verdictwrite

The per-version scan verdict the gate records, read before a download is served.

Audit logappend

Upload, an infected detection, an upload refused because the scanner could not be reached, and download events.

Virus scannerinternal

A self-hosted scan service the app calls.

Security decisions

  • Files are envelope-encrypted at rest (AES-256-GCM, a per-object key wrapped by a master key).
  • Content type is admitted by an allowlist plus a magic-byte check of the first bytes; archives, executables, HTML and SVG are refused at the allowlist, so there is no inline-active-content path.
  • A single 50 MB cap is checked before and during the stream; the file name is sanitised and never used as the storage key (keys are system-generated and a path-traversal key is rejected).
  • Each customer's files are in their own storage container.
  • Downloads are served as attachments over an authenticated session, never inline and never from a public URL.
  • Every upload is scanned by the virus scanner; a file reported infected is blocked on download and audited, and an upload is refused if the scanner cannot be reached.

Subtasks

  • Upload path with per-slot allowed types, a 50 MB cap, name sanitising and a magic-byte content check.
  • Envelope-encrypted, versioned file store with a per-customer container.
  • Virus-scan adapter to a self-hosted scan service.
  • A post-store scan gate that scans every upload, blocks a file reported infected on download, and refuses an upload if the scanner cannot be reached.
  • Access-scoped download served as an attachment.

How each scenario is verified

  • Admission: type, size and name
    An upload-admission test rejects an off-allowlist content type and archives, sanitises the file name, refuses a non-octet-stream transport, and stops the hashing stream at the 50 MB cap.
  • Content type is checked by bytes, not name
    A content-sniffing test accepts a valid head, rejects an executable head for an allowed type, rejects a cross-type mislabel, and rejects a binary head for a text type.
  • The scanning step blocks an infected file
    An integration test against a scan daemon stores and serves a clean file, and records, audits and hard-blocks an infected file; an unreachable scanner refuses the upload and records the attempt.
  • Encryption at rest
    An envelope-encryption test proves only ciphertext is written, and tamper, truncation or substitution yield no plaintext; the same plaintext twice gives different ciphertext.
  • Per-customer storage
    A storage test round-trips ciphertext, gives two customers separate containers, and treats a non-customer-scoped key as an invariant breach.
  • End-to-end documents
    A documents integration test stores ciphertext and recovers plaintext with a matching hash, blocks an infected version on download, and never inherits a prior verdict on re-upload.
  • Download scoped to the owner's team
    Verified by review of the access-scoped, attachment-only retrieval path.

Risk tier

Core A person approves every merge. This is a Core-tier change: it handles untrusted files, so it also carries the fullest set of automated tests; a regression here affects the whole product, not one page.

Start here

Want this on your backlog?

Get a free baseline report on one repository, or scope a 3-month pilot measured against what your team’s work costs today.

Measured from your own git history
Every pull request tested before review
A person approves every merge