Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions docs/PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
FileSentry is a secure file-ingestion and malware-scanning API built with ASP.NET
Core, PostgreSQL, ClamAV, and Docker.

**Status:** MVP behavior, security hardening, automated tests, and GitHub Actions CI
are implemented. Final local verification is complete; release blockers are recorded
in `docs/ai/CURRENT_TASK.md`.
**Status:** The MVP, local release verification, and hosted default-branch CI are
complete. The remaining pre-tag repository check is recorded in
`docs/ai/CURRENT_TASK.md`.

**Architecture:** .NET 10 modular monolith with a database-backed scanner worker.

Expand Down Expand Up @@ -151,12 +151,12 @@ grows.
4. Owner-authorized metadata, download, and delete operations.
5. Audit, correlation, rate limiting, operational hardening, and CI.

### Current phase: final verification
### Current phase: release readiness

- repeat the full release checklist from a clean worktree/clone;
- verify migrations and the documented manual API flow;
- inspect dependency, secret, runtime-artifact, and configuration hygiene;
- confirm the default-branch hosted CI result and record remaining limitations;
- record the successful default-branch hosted CI result and remaining limitations;
- decide separately whether to tag or publish a release.

Local verification results and release blockers are recorded in
Expand All @@ -169,8 +169,8 @@ Local verification results and release blockers are recorded in
- [x] Clean and infected content follows the correct storage lifecycle.
- [x] Metadata, download, and delete enforce owner predicates.
- [x] Audit, correlation, and focused rate limits are implemented.
- [ ] The current default-branch GitHub Actions run is independently observed green;
the secret-free workflow is present and the complete suite passes locally.
- [x] The default-branch GitHub Actions run is observed green; the secret-free
workflow and complete local suite pass.
- [x] Storage, `.env`, User Secrets, build output, and malware samples are excluded
from source control.
- [x] MIT license is present.
Expand All @@ -182,21 +182,23 @@ Local verification results and release blockers are recorded in
These requirements are retained for final verification; an item is not complete
merely because an implementation or document claims it is.

- [ ] Core acceptance criteria pass.
- [ ] Release build succeeds without warnings selected as errors.
- [ ] Unit and integration test results are recorded.
- [ ] GitHub Actions is green on the default branch.
- [ ] Docker images run as non-root where feasible.
- [ ] ClamAV is internal-only.
- [ ] Database migrations are reproducible.
- [ ] `.env.example` and configuration documentation are complete.
- [ ] Threat model reflects the implemented behavior.
- [ ] README includes architecture, setup, API examples, security controls, and
- [x] Core acceptance criteria pass.
- [x] Release build succeeds without warnings selected as errors.
- [x] Unit and integration test results are recorded.
- [x] GitHub Actions is green on the default branch.
- [x] Docker images run as non-root where feasible.
- [x] ClamAV is internal-only.
- [x] Database migrations are reproducible.
- [x] `.env.example` and configuration documentation are complete.
- [x] Threat model reflects the implemented behavior.
- [x] README includes architecture, setup, API examples, security controls, and
limitations.
- [ ] Repository has a license, description, and relevant GitHub topics.
- [ ] Demo video is linked.
- [ ] Tagged release is created.

A recorded demo video is intentionally outside the release scope. The written
[`demo.md`](demo.md) guide remains the repeatable demonstration artifact.

## Limitations

- A ClamAV clean result is not proof that content is harmless.
Expand Down
75 changes: 32 additions & 43 deletions docs/ai/CURRENT_TASK.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Current Task: Final MVP Verification
# Current Task: Final Release Readiness

## Outcome

Final local verification completed on 2026-08-14 from an isolated clone of commit
`8bf1218`. FileSentry is **not yet ready to tag as `v1.0.0`**: the demo video
required by the Definition of Done is not linked, and the current default-branch
GitHub Actions result plus remote repository description/topics were not observable
from this environment.
The FileSentry MVP's technical acceptance criteria are complete. Local final
verification passed, and GitHub Actions has now passed on the merged default branch.
A demo video will deliberately not be created and is no longer a release
requirement; [`docs/demo.md`](../demo.md) remains the written repeatable guide.

No application feature, commit, push, tag, release, or remote setting was created
or changed.
FileSentry is **not yet ready to tag as `v1.0.0`** because the remote repository
description and relevant GitHub topics have not been verified. No application
feature, commit, push, tag, release, or remote setting was created or changed.

## Acceptance criteria

Expand All @@ -20,57 +20,46 @@ or changed.
| 3 | PASS | A live clean PDF was promoted and downloaded byte-for-byte; a valid PDF stream containing runtime-assembled canonical EICAR became `Infected`, storage state `Deleted`, with no bytes under clean or quarantine. |
| 4 | PASS | Owner metadata/download/delete worked; a second authenticated user received identical `404` results for metadata, download, and delete. PostgreSQL-backed endpoint tests passed. |
| 5 | PASS | Live audit rows recorded upload/scan/clean/malware/failure/download/delete with file, actor where applicable, and persisted correlation IDs. Deterministic limits returned `AUTH_RATE_LIMITED` and `UPLOAD_RATE_LIMITED`. |
| 6 | NOT VERIFIED | The least-privilege, secret-free workflow is present and all 106 tests pass locally, but the current hosted GitHub Actions result was not observable (`gh` unavailable and the unauthenticated repository API returned `404`). |
| 6 | PASS | The least-privilege, secret-free workflow passed successfully on merged `main`; the same complete 106-test suite passes locally. |
| 7 | PASS | Tracked-file inspection found no `deploy/.env`, runtime `data`, build/test output, private keys, JWT-like values, or contiguous raw EICAR sample; ignore checks cover `.env` and runtime storage. |
| 8 | PASS | `LICENSE` contains the MIT license. |
| 9 | PASS | The documented setup was exercised in an isolated clone without inherited `.env`, storage, package cache, or developer configuration; restore/build/test/format/migrations/Compose and API startup succeeded. |
| 10 | NOT APPLICABLE | A tag or GitHub release is a later explicitly approved action and was prohibited for this milestone. |
| 10 | INCOMPLETE | No tag or GitHub release has been created; that remains a separate explicitly approved action. |

## Definition of Done

| Item | Status | Evidence |
|---|---|---|
| Core acceptance criteria pass | NOT VERIFIED | All locally testable core behavior passed; the hosted-CI acceptance criterion remains unverified. |
| Release build succeeds without warnings selected as errors | PASS | Isolated `Release` build completed with 0 warnings and 0 errors. |
| Core acceptance criteria pass | PASS | All technical acceptance criteria passed locally and hosted CI is green on the default branch. |
| Release build succeeds without warnings selected as errors | PASS | Release verification completed with 0 warnings and 0 errors. |
| Unit and integration test results are recorded | PASS | 31 unit and 75 integration tests passed; 106 total, 0 failed, 0 skipped. |
| GitHub Actions is green on the default branch | NOT VERIFIED | Current hosted status was not accessible from this environment. |
| Docker images run as non-root where feasible | PASS | Runtime inspection showed PostgreSQL as UID 999 and `clamd`/`freshclam` as the unprivileged `_rpc` account; the upstream ClamAV init/tail helpers remain root. |
| GitHub Actions is green on the default branch | PASS | The hosted workflow passed successfully on merged `main`. |
| Docker images run as non-root where feasible | PASS | PostgreSQL ran as UID 999 and `clamd`/`freshclam` as the unprivileged `_rpc` account; upstream ClamAV init/tail helpers remain root. |
| ClamAV is internal-only | PASS | Compose binds ClamAV only to `127.0.0.1`; live inspection confirmed the loopback binding. |
| Database migrations are reproducible | PASS | All five migrations applied to fresh PostgreSQL; a repeat update reported the database current. |
| `.env.example` and configuration documentation are complete | PASS | The example contains local placeholders only; README documents `.env`, User Secrets, validated settings, migrations, and startup. |
| Threat model reflects implemented behavior | PASS | Threat, control, and residual-risk statements match inspected code and live results. |
| README includes architecture, setup, API examples, security controls, and limitations | PASS | All required sections are present, including that a clean result is not proof of harmlessness. |
| Repository has a license, description, and relevant GitHub topics | NOT VERIFIED | MIT license is present; remote description and topics were not observable. |
| Demo video is linked | FAIL | `docs/demo.md` contains a repeatable script but no video link. |
| Tagged release is created | NOT APPLICABLE | Tagging/releasing was explicitly prohibited and belongs after release approval. |
| Repository has a license, description, and relevant GitHub topics | NOT VERIFIED | MIT license is present; remote description and topics remain unverified. |
| Tagged release is created | INCOMPLETE | No tag or release exists yet, as required for this pre-tag milestone. |

## Executed verification
The former demo-video item was intentionally removed from the Definition of Done.
No nonexistent video is claimed as complete or required for release.

- `dotnet tool restore`: restored `dotnet-ef` 10.0.11.
- `dotnet restore FileSentry.slnx`: passed from an empty isolated package cache.
- `dotnet build FileSentry.slnx --configuration Release`: passed, 0 warnings and
0 errors.
- `dotnet test FileSentry.slnx --configuration Release`: passed 31 unit and 75
integration tests (106 total), including real PostgreSQL and ClamAV containers.
- `dotnet format FileSentry.slnx --verify-no-changes`: passed.
- `dotnet ef migrations list --project src/FileSentry.Api`: listed all five
migrations; `dotnet ef database update --project src/FileSentry.Api` applied them
to fresh PostgreSQL and a repeat run was a successful no-op.
- `docker compose --env-file deploy/.env -f deploy/docker-compose.yml config
--quiet`: passed with an isolated ignored environment file.
- NuGet direct/transitive vulnerability audit: no vulnerable packages reported by
the configured sources.
- Fresh PostgreSQL and ClamAV containers became healthy; the API started from the
isolated clone and returned live/ready `200` under normal conditions.
- `git diff --check`, local Markdown-link validation, and final status/diff
inspection passed.
## Verification record

## Release blockers
- Isolated restore, zero-warning Release build, all 106 tests, formatting,
migrations, Compose validation, dependency health, and the NuGet vulnerability
audit passed during final verification.
- Clean, infected, retry-exhausted ClamAV-outage, ownership, deletion,
audit/correlation, and authentication/upload rate-limit flows passed.
- GitHub Actions passed on the merged default branch without repository secrets or
developer User Secrets.
- The release-readiness Release build passed with 0 warnings and 0 errors; all 106
tests passed again. `git diff --check` passed after the documentation update.

1. Record an observable green GitHub Actions run for current `main`/release commit.
2. Add the required demo video link, or explicitly remove that requirement through
a deliberate project-plan decision.
3. Verify or configure the remote repository description and relevant topics.
## Remaining release blocker

After those items are resolved, repeat only the affected remote/documentation checks
and approve the separate `v1.0.0` tag/release action.
Verify or configure the GitHub repository description and relevant topics. Once
that repository-level item is complete, FileSentry is ready for a separately
approved `v1.0.0` tag/release action.
8 changes: 4 additions & 4 deletions docs/ai/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ User Secrets (or equivalent external configuration) for the database connection
string and JWT signing key.

Release documentation now lives in the README plus focused architecture, threat
model, demo, and project-plan files. The current hosted CI result and remote
description/topics were not observable during final verification, and the required
demo video is not linked. These are release blockers; no tag or GitHub release was
created.
model, demo, and project-plan files. GitHub Actions has passed on merged `main`.
A recorded demo video is intentionally outside project scope; `docs/demo.md` remains
the repeatable written guide. Remote repository description/topics remain
unverified, and no tag or GitHub release has been created.

## Limitations and explicit non-goals

Expand Down
18 changes: 14 additions & 4 deletions tests/FileSentry.UnitTests/ClamAvClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,26 +199,36 @@ private static async Task<ClamAvScanResult> RunWithServerAsync(
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
Task serverTask = Task.Run(async () =>
Task serverTask = RunServerAsync();

async Task RunServerAsync()
{
Task<TcpClient>? versionAcceptTask = null;
using (TcpClient scanClient = await listener.AcceptTcpClientAsync())
await using (NetworkStream stream = scanClient.GetStream())
{
byte[] received = await ReadInStreamContentAsync(stream);
onContent?.Invoke(received);
if (versionResponse is not null)
{
// The client opens VERSION immediately after reading the scan response.
versionAcceptTask = listener.AcceptTcpClientAsync();
}

await stream.WriteAsync(Encoding.ASCII.GetBytes(scanResponse));
}

if (versionResponse is not null)
if (versionResponse is not null && versionAcceptTask is not null)
{
using TcpClient versionClient = await listener.AcceptTcpClientAsync();
using TcpClient versionClient = await versionAcceptTask;
await using NetworkStream stream = versionClient.GetStream();
byte[] command = new byte[VersionCommand.Length];
await stream.ReadExactlyAsync(command);
Assert.Equal(VersionCommand, command);
await stream.WriteAsync(Encoding.ASCII.GetBytes(versionResponse));
}
});
}

var client = new ClamAvClient(Options.Create(CreateOptions(port)));

ClamAvScanResult result = await client.ScanAsync(
Expand Down