The Project Management screen (/pm, view only) with Overview, Projects & units, Bottlenecks & aging, Hours vs target, Quality & issues and Find jobs, the period picker, health per unit and per project, drill down, Excel (CSV) per table and for all, and the PESCO production status report PDF with the controlled-copy stamp; every export logged as "report exported". The find bar on every home screen: the full search set, the dropdowns, quick flags with Mine, the flag kinds, Board and List with sorts, filters saved per browser; every filter applied by the server on the list and the boards, with facets. Presence in packets with role, lock and revoke drop; the motion switch with reduced motion honoured, card glides, glow and count-up; Back restores the scroll spot; sheet placement and the iPad layout pass. The data book compared page by page with the old build and six layout differences fixed. Tests for the PM aggregates, search, filters, presence and exports; Playwright scenarios for the PM screen, the find bar and presence at desktop and iPad viewports. Docs updated; PLAN.md questions 38 to 42. Co-Authored-By: Claude Fable 5.1 <[email protected]>
29 KiB
Packet Station security
This document is rewritten for the server build. The old browser build's security document is in the old repo (C:\Users\spencerm\Coding\PacketStation\docs\SECURITY.md). The design comes from BRIEF.md and PLAN.md.
Status: phase 8. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Phase 4 completes the upload pipeline (a kind per upload with its own types and limit, XLSX and CSV by content, the optional ClamAV scan, the assets volume quota alert) and adds the warehouse rules (the heat gate on release, closed hold and short reasons, receive by custody). Phase 4 closes D5. Phase 5 adds the shop rules (the server as the clock of record, server-assigned mark numbers under the packet row lock, insert-only change-order verifications with a signature, VOID sheets never edited). Phase 6 adds the Q.C. rules (server gates on accept, block sign and sign-off, NCR numbers checked by pattern, Q.C. documents never deleted, the append-only ITP log, server-verified signatures on every Q.C. and ITP sign-off). No requirement ID closes in phase 6. Phase 7 adds the close and file controls (the HMAC seal with the data book filed on it, the controlled-copy and print stamps signed with the seal key, print_log and audit rows for every print and export, the CSV formula guard, PDF metadata, escaping in the PDFs). Phase 7 closes M2, S6, S7 and S8. Phase 8 adds the Project Management reads and exports, the server-side find bar filters and the presence rules (display name and role only, locked and revoked sessions dropped). No requirement ID closes in phase 8. Every other control names the phase that builds it.
Contents:
- Part 1: the 20 old layers and where each one lives now.
- Part 2: the new build's controls.
- Part 3: the production requirement IDs and the phase that closes each.
Part 1: the old layers, moved to the server
The M: drive build had 20 layers of protection, most of them enforced in the browser. Every layer that still applies moves to the server. The rest are dropped or replaced.
| # | Old layer | Now | Phase |
|---|---|---|---|
| 1 | PESCO network and firewall | Kept. The app runs on docker-1 inside the PESCO network, reached only through Nginx Proxy Manager. It publishes no port. PostgreSQL is on each stack's internal network only. |
0 |
| 2 | Windows folder rights on the M: folder | Dropped. Replaced by database roles: the app role cannot change the schema or update or delete the audit; migrations run as a separate role. Files sit on a volume only the containers see. | 0 |
| 3 | Employee ID + PIN sign-in | Replaced by AD sign-in with the domain password, plus an optional Packet Station PIN checked on the server, for registered shop devices only. | 1 |
| 4 | Department and level screens | A static role-to-screen map in the browser, plus server policy on every request. The browser only decides what to show. | 1, 2 |
| 5 | Per-PC access log | Replaced by the server audit. Each row carries the AD account, session, client IP and user agent. No per-PC browser ids. | 1 |
| 6 | Filing seals and filed-packet lock | HMAC seals computed on the server with a server key. The filed lock is a server policy rule. Built. | 2, 7 |
| 7 | Record integrity check | A worker job (seals, asset hashes, audit hash chain) with an alert. | 9 |
| 8 | Self-hosted libraries | Kept: no CDN. The CSP allows nothing external. | 0 |
| 9 | Output escaping | React escapes output by default. The PDF builders clean and escape every text on the server. Built. | 1, 7 |
| 10 | Central permission policy | Server policy, checked on every write, including writes that do not come from the screens. Built. | 2 |
| 11 | PIN sign-off (electronic signature) | Server-verified signature with the PIN or the domain password, stored with who, when, what, factor and a content hash. | 1 |
| 12 | IT edit reasons | Kept. Admin writes need a reason and are marked as IT edits in the packet history and the audit. | 2 |
| 13 | Screen lock | Server-enforced sessions. The browser reports idle and background state; the server records it and refuses an idle or locked session. Unlock is a step-up check on the server. | 1 |
| 14 | Input and upload validation | On the server: field limits on every write, closed reason sets, uploads typed by content per kind (drawing PDF; MTR and document PDF, PNG or JPEG; BOM XLSX or CSV), a size limit per kind (25 MB, BOM 5 MB), sanitised names, an optional ClamAV scan before anything is stored, and a quota alert on the assets volume. Built. | 2, 4 |
| 15 | Second-admin approval | Superseded. Roles come only from AD groups and the group-to-role map is derived, so there is nothing to grant in the app. | 1 |
| 16 | Security alerts and automatic integrity check | Worker jobs and the Notifications tab (email through app-03). | 9 |
| 17 | Controlled-copy stamp on data books | Applied to server-built PDFs and signed with the seal key. Built, phase 7. | 7 |
| 18 | CSV formula guard | Applied to every server export. Built, phase 7. | 7 |
| 19 | Update lock and stale-version block | Replaced by a build version endpoint. A screen on an older build shows a banner and reloads. | 0 (endpoint), 10 (banner) |
| 20 | VISUAL helper write guard | Dropped. There is no helper. VisualKit and MFilesKit are read-only by design and connect as read-only accounts. | 3 |
Part 2: the new build's controls
Sessions (phase 1)
- HttpOnly, SameSite=Strict session cookie. The session row on the server decides.
- Password sessions: idle sign-out after 15 minutes, absolute 12 hours. PIN shop sessions: absolute 10 hours, idle 15 minutes. All are settings.
- Idle and background lock are reported by the browser and enforced by the server. Unlock is a step-up check with the PIN or the domain password.
- Origin check on every state-changing request: the
Originheader (or theRefererorigin) must match the request host, andX-Requested-With: PacketStationmust be present. Otherwise 403. - Admin is a password session only. A PIN session cannot open Admin or change Account settings.
- Roles are resolved from AD at sign-in (nested groups through
LDAP_MATCHING_RULE_IN_CHAIN), cached on the session, and re-resolved every 15 minutes on the session and by the AD refresh. A disabled AD account is always refused. A session whose user has lost every role is ended at the next refresh. - The session cookie token is stored only as an HMAC with the session key. The AD password is used for the bind and discarded.
- The group-to-role map is derived from the department table and the
PS-naming convention at startup. No screen edits it, so nobody can promote a group or themselves inside the app. - The browser reports hidden tabs and idle time; the server records
locked_sinceand refuses a locked session with 423 until a step-up unlock. The server also locks a session itself when no request arrived for the lock interval (3 minutes, as the old build's screen lock), and when a tab stayed hidden for over a minute (the old build's background lock). A reload or an in-app navigation never locks.
PIN (phase 1)
- Optional, per user, set by the user in Account settings after a password sign-in. Setting or changing it needs the domain password in the same request.
- 6 to 12 digits, hashed with Argon2id on the server. Never visible to Admin. Admin can clear a PIN (audited), not set one.
- PIN sign-in uses the employee ID and works only on registered devices. Devices are registered by an Admin or a shop department Supervisor in a password session.
- Five wrong PIN attempts in a row, across sign-in, unlock and signature, disable the PIN. Three wrong attempts at unlock or signature end the session. A guessed PIN never touches AD.
- A PIN sign-in still checks AD with the service account: a disabled account, or one removed from every group, is refused.
- The device cookie is a random token stored as an HMAC; revoking a device ends every PIN session on it.
Rate limits (phase 1)
- Checked before any bind to AD, so the app can never lock a domain account out.
- Defaults: 5 failed password sign-ins per account per 5 minutes, then a 10-minute cooldown. 30 failed attempts per client address per 5 minutes. Successful sign-ins never count.
- The domain's own lockout (20 attempts in 5 minutes) stays the last line.
- The client address comes from
X-Forwarded-Foronly when the request comes fromnpm_app_1itself, re-resolved on change. Other containers onnpm_defaultare not trusted. - A refused attempt answers 429 with
Retry-After, is recorded asrate_limitedand never counts toward the limits itself. - Failed sign-ins answer one message for an unknown account, a wrong password and a disabled account, so accounts cannot be enumerated. The audit and
signin_attemptskeep the real reason.
Policy (phase 2)
- One policy in Python on the server (
server/packetstation/policy/), pure functions with no I/O. Every write goes: session, policy, transition, validation, version check, packet log row, audit row, NOTIFY, in one transaction with the packet row locked. Nothing in the browser decides. - Every packet read returns
can: [...],blocked: {...}andholder, so the browser enables controls from the server's answer. The server re-checks on the write. - A workflow step taken twice is refused with 409 and the current state (compare-and-set on the expected status and department). Child rows carry a row version checked with If-Match. Weld, heat tag, page and change order numbers are assigned by the server under the lock.
- Preconditions (heat map answered, drawing attached, BOM pulled or short, heats recorded on pulled lines, checklist complete, welds recorded, Q.C. items complete, closed in VISUAL before filing) run on the server and are never bypassed, Admin included.
- Admin edits outside custody need an IT reason of at least 8 characters, recorded in the packet log and the audit row (
it_reason). An Admin cannot write a filed packet except through the reopen action, which needs a reason and a signature and breaks the seal. - Signatures (PIN or domain password, verified in the same transaction) on submit, send to Q.C., Q.C. accept and reject, route, file, reopen, change order issue, approve, reject and verify, the revision approval and hand drawing uploads.
- Input: every string is cleaned (control and invisible characters removed, NFC, trimmed) and cut to its field limit; heat numbers, stamps, project numbers and lots are checked against the old patterns; JSON bodies over 200 KB are refused.
- Uploads: typed by content (PDF, PNG, JPEG), 25 MB limit read in chunks, sha256, stored by id under the assets volume, served only with a session and with
Content-DispositionandX-Content-Type-Options: nosniff. Phase 4 completes it (see "Uploads and storage"). - The filed lock: a filed packet accepts only the data book date and the Admin reopen. Filing writes an HMAC-SHA256 seal with the server seal key; a reopen keeps the old seal with who broke it and why.
- Details in POLICY.md.
Uploads and storage (phase 4)
- Every upload names a kind (
purpose): drawing, mtr, document or bom. No purpose means document. An unknown kind is 422. - The type is read from the content, never from the file name or the browser's type. A drawing is a PDF. An MTR or a document is a PDF, PNG or JPEG. A BOM is an XLSX or a CSV. A wrong type for the kind is 415
unsupported_typewith a message that names the allowed types. - An XLSX must be a zip that lists
[Content_Types].xmlandxl/workbook.xml; a plain zip is refused. A CSV has no magic, so it is accepted only as a BOM, and only when its first 64 KB decode as UTF-8 with no NUL and no control character other than tab, CR and LF. - Limits per kind: 25 MB (
upload_max_bytes) for drawing, mtr and document, 5 MB (upload_max_bytes_bom) for a BOM. The Content-Length is checked first, then the bytes as they are read. Over the limit is 413too_large. - XLSX and CSV are always served as an attachment, never inline. Every blob keeps
Content-Dispositionandnosniff. - Optional ClamAV (requirement D5). When
CLAMAV_HOSTis set, every upload and every file the worker stores (drawings from M-Files) is streamed to clamd withzINSTREAMafter it is read in full and before it is deduped or placed. A hit is 422infected, nothing is stored, and anasset.rejectedaudit row keeps the signature, size and type. A scanner that cannot be reached refuses the upload with 503scan_unavailable(fail closed). WithCLAMAV_HOSTempty there is no scan. - Quota alert. After every stored file and in the housekeeping job, the assets volume usage is checked against
ASSETS_QUOTA_WARN_PCT(85 by default). Over it, one opennotification_eventsrow of kindassets_quotais written and audited asstorage.quota_warning. Back under, the row is resolved. A failed check never fails an upload. Admin sees usage, the alert and the scanner on the Storage tab and can acknowledge an alert (alert.acknowledged). Phase 9 sends the email.
Warehouse rules (phase 4)
- Release to the supervisor is refused on the server until every line is pulled or short and every pulled line that issued pieces has a heat (N/A and sub-assembly lines excepted). Heat numbers are checked against the old pattern.
- Hold reasons and short reasons are closed sets on the server.
- Short receipts go to the warehouse in any status but filed, or to the holder of the packet through the BOM edit rule. Nobody else.
- An MTR on a line must be a PDF, PNG or JPEG asset.
Shop rules (phase 5)
- The server is the clock of record. The station clock is kept in
packet_stageswith the database time. Only the server writes the clock columns. No request field sets the elapsed time. - Weld and heat tag numbers (
W<n>,H<n>) are assigned by the server from the packet counters, under the packet row lock. Two people marking at once never get the same number. - ASSIGN checks on the server that the welder belongs to the department and that a limited worker's station is one of theirs.
- A change-order verification needs a signature and is insert-only. A second one for the same department is refused. It cannot be withdrawn.
- VOID sheets are never edited. A voided page is kept, and new marks or moves on it are refused (422
void_page). - A borrowed heat map is read-only. Nothing on the sibling packet can be written through the borrowing packet.
- Presence over SSE carries display names, never other personal data. Each entry is the user id, display name, screen and packet id. Phase 8 adds the role and since (see "Project Management, find bar and presence").
Q.C. rules (phase 6)
- Accept is refused on the server until the Q.C. record is complete (
qc_missing_empty: every check item, a visual result on every weld and none rejected, an acceptable NDE result when a method is set, the inspector name). The screen only shows the reason. - A block signature is refused until every line of the block is initialed or N/A. The sign-off is refused until every block is signed. Both are server rules. A signed-off ITP refuses every write but the reopen, which needs a Q.C. Supervisor, or an admin with an IT reason, and a reason.
- An ITP line rejected without an NCR number cannot be approved (server check). Line initials and block signatures can be removed only by the person who made them, or an admin.
- NCR numbers are checked against the pattern
NCR-YYYY-NNNNon the server. Checking them against pesco-ncr is reserved for a later phase (domain/ncr.py,validate_ncr). The NCR link opens in a new tab withrel="noopener noreferrer". - Documents are never deleted. A packet document is taken out of the data book with a reason and the row is kept. A Q.C. folder document is replaced by a new revision; the old row stays and points at the new one. Packet document kinds and Q.C. document types are closed sets, checked on the server and by database CHECKs. Every document file is a PDF, PNG or JPEG, typed by content.
itp_logis append-only, likepacket_log: the app role has SELECT and INSERT only, and a trigger refuses updates, deletes and truncate.itp_rejectsstays append-only, so every reject and its NCR number survive a later approval.- Signatures verified on the server, with a content hash: Q.C. accept, Q.C. accept and route, Q.C. reject, the ITP block signature and the ITP sign-off.
- The ITP and the Q.C. folder are checked with
check_scoped(): the same system, admin bypass, who and precondition steps as a packet write. An admin outside Q.C. and Clerical needs an IT reason, recorded initp_logand the audit row. - Every ITP write and every Q.C. folder document writes an audit row (
itp.<action>,qc_doc.add,qc_doc.revise).
Worker (phase 3)
- The worker publishes no port and joins no proxy network. It reaches out to VISUAL and M-Files only; nothing reaches in. Admin and Clerical reach it only through rows in PostgreSQL (
job_requests, thefeedsettings row) and NOTIFY. - It reads VISUAL through VisualKit as
visualkit_roand M-Files through MFilesKit asmfileskitRO. Both libraries are read-only in their own code. Nothing in Packet Station writes to either. - Stack guard at start and again before every feed run: the test stack refuses a production VISUAL target, production refuses an implicit one, and production refuses a fixture source.
- One run per job at a time, by a PostgreSQL advisory lock per job name. Feed writes run in one transaction with a lock timeout and skip rows a person has locked.
- Job errors are redacted (connection strings,
password=,Authorization:and the VISUAL and M-Files passwords themselves) and cut to 20 entries of 500 characters before they are stored or shown. - Every feed write goes through the server policy as the system principal, which can take only the
vis_*actions. Each one writes a packet log row and an audit row with rolesystem. Each run writes afeed.runaudit row with its counts, and each drawing fetch adrawing.fetchedrow with the sha256. Feed settings and resource map changes are audited with before and after. - Downloaded drawings go through the same checks as uploads: PDF magic, size limit, streamed sha256, stored by id.
Audit (phase 1)
- Append-only
audit_events, with a hash chain (prev_hash,row_hash). - The app role may insert and select, never update or delete. A trigger also refuses updates and deletes. Migrations run as a separate role.
packetstation doctorchecks that an audit UPDATE fails.- Every identity event is a row: sign-in, failed sign-in with a reason, sign-out, lock, unlock, session ended, PIN set, disabled or cleared, device registered or revoked, signature, AD refresh. Each carries the user, session, roles, client address and a small detail object, never a secret.
signin_attemptsandsignaturesare append-only in the same way.- Phase 4 audit actions:
asset.rejected(an upload the virus scan refused, withreason,signature,sizeandmime),storage.quota_warning(the assets volume went over the warning threshold, with the usage) andalert.acknowledged(an Admin acknowledged an open alert). Every warehouse write is apacket.<action>row withdetail.item, the warehouse stage before and after, anddetail.qtyon short writes.
CI identity fixtures (phase 1)
- CI runs a throwaway Samba Active Directory domain controller (
PSTEST.LOCAL) built fromdeploy/samba/, with a generated CA so the LDAPS path is real. It is seeded with the 50 groups (one missing and one extra on purpose) and test users for every role, including nested, disabled, multi-role, missing and duplicate employee ID cases. Passwords are random per run and exported to a volume the tests read. PESCO's AD is never used by CI.
Seals (phase 7)
- Filing writes an HMAC-SHA256 seal over the packet's read model in seal mode, with the server seal key (
SEAL_KEY_FILE, a Docker secret) and itskey_id. Nobody without the key can recompute it (M2).GET /api/packets/{id}/sealrebuilds the content and says whether the seal still matches. - The data book is built on the server inside the filing transaction and stored as an asset on the seal. A data book that cannot be built fails the filing; nothing is filed half way.
- Filing while the feed shows a work order open in VISUAL needs a second confirmation. The work orders are kept on the seal and in the packet log.
- A reopen by Admin needs a reason and a signature. It marks the seal broken with who, when and why, and keeps the row and its data book. Filing again writes a new seal beside it.
- The integrity job checks seals and asset hashes (phase 9).
PDFs, prints and exports (phase 7)
- Every PDF and the CSV are built on the server. The browser only opens the URL. Reads need a signed-in session; every role that can open the packet may read them, PM included.
- Controlled-copy stamp (S7): every exported page carries
Controlled copy - exported by Name (emp) <UTC time>, the seal's first 12 hex ornot yet filed, and- stamp <sig>. The filed data book is the stored copy, served as it was built. - Printed-copy stamp (S8): every printed page carries
PRINTED COPY - <what> - printed by Name (emp) <UTC time> - <project> Lot <unit> dwg <drawing> R<rev> - verify revision before useand the stamp signature, in a white box at the bottom of the page. - Stamp signature: 16 hex of HMAC-SHA256 with the seal key over the packet, the kind (a printed copy signs
print:<kind>, so a print and an export of the same thing in the same minute never share a signature), the seal hash, the user and the time. A copy can be checked against the server later withverify_stamp; the Admin screen for it comes in phase 9. - Logging (S7, S8): every print and export writes one
print_logrow (who, when, session, packet, unit or ITP, kind, what, pages, the stamp line, the stamp signature, the client address) and one audit row (printorexport).print_logis append-only: the app role has SELECT and INSERT only, and a trigger refuses updates, deletes and truncate. - CSV formula guard (S6): a cell that starts with
=,+,-,@, a tab or a CR gets a leading'. A cell with a quote, a comma or a line break is quoted with the quotes doubled. - PDF metadata: title, subject and keywords name the packet and the stamp signature; Author is PESCO and Producer is Packet Station.
- Output escaping (S5): every text drawn into a PDF goes through
clean(control characters removed, characters outside Latin-1 replaced) and is XML-escaped where a Paragraph renders it, so data from VISUAL, M-Files or a person cannot inject markup. - Responses carry
Content-Disposition(inline, or attachment with?download=1),X-Content-Type-Options: nosniffandCache-Control: no-store. A print of a document checks that the asset belongs to the packet, its unit or project folder, or its filed data book; anything else is 404. - The signed ITP PDF is built and filed in the sign-off transaction. A PDF that cannot be built fails the sign-off.
Project Management, find bar and presence (phase 8)
- Project Management reads only. Every endpoint under
/api/pmis a GET and needs a signed-in session. Any role may read them, as any role may open any packet. The PM role passes no write action anywhere. The reports read PostgreSQL only, never VISUAL or M-Files. - Every PM export is built on the server and logged: one
print_logrow (kindreport-csvorreport-pdf, no packet, the scope label inwhat, the page count for the PDF) and one audit row with the actionreport exportedand the detail{name, format, scope, period}.print_logstays append-only. - The PM CSV has the formula guard on every cell (S6), a BOM and CRLF line ends. The PDF report carries the controlled-copy stamp on every page and the PDF metadata. Both answer with
Content-Disposition,nosniffandno-store, as the phase 7 exports do. - Filters never widen access. The search, the status, flag, station, worker and Mine filters are applied by the server inside each list's and board's own scope (a supervisor's departments, the stall's station). A filter can only narrow what the person may already read.
- Search words are bound parameters, matched with ILIKE and the LIKE escape. A query keeps at most 8 words of at most 60 characters each. IDs are matched as text, never cast.
- Mine is decided on the server from the person's AD roles and the screen they name. Nothing the browser keeps can widen it.
- Presence shows the display name and the role only (the user id, screen, packet id and since travel with it). No employee ID, no email, no other personal data. The server builds it from the session, not from what the browser says about itself.
- A locked session drops out of presence at once, and comes back on unlock. A revoked session drops out at once and its stream ends. A closed tab drops out when the stream ends, within the 25-second ping cycle.
- Saved filters live in the browser's localStorage (
ps.flt,ps.proj,ps.pmtab,ps.pmper,ps.motion). They hold filter choices only. The search text is never saved. Nothing in them is trusted by the server.
Headers (phase 0)
- The app sets HSTS, CSP and the other security headers itself. NPM's own HSTS is off.
- The CSP allows nothing external. It is tuned again in phase 10.
Secrets (phase 0)
- Docker secrets only, files under
/etc/packetstation/<stack>/secrets, ownerroot:10001, mode0440. - Read through
*_PASSWORD_FILEsettings. MFilesKit's password is exported by the entrypoint from its secret file. - Never in the repo,
.env, logs or chat. See DEPLOY.md.
Part 3: requirement IDs
The IDs come from Part 3 of the old SECURITY.md. The status follows the approved plan.
| ID | Status |
|---|---|
| H1, H2 | Closed, phase 0 (docker-1, internal DNS, NPM) |
| H3 | Closed, phase 0 (NPM TLS, HSTS from the app) |
| H4 | Closed, phase 0 (two stacks) |
| H5 | IT's (host patching); documented in DEPLOY.md |
| I1 | Partly closed, phase 1 (AD sign-in, no SSO); Entra SSO deferred |
| I2 | Deferred (MFA with Entra) |
| I3 | Closed for "PIN on the server", phase 1; device certificate deferred |
| I4 | Closed, phase 1 |
| I5 | Superseded (no in-app admin grant); separate IT admin accounts remain IT's |
| I6 | Largely closed, phase 9 (groups plus reconcile); quarterly review IT's |
| I7 | Closed, phase 1 |
| I8 | App part closed, phase 1; kiosk lock policy IT's (Intune or GPO) |
| A1, A2, A3 | Closed, phase 2 (server policy on every call, workflow steps as compare-and-set, validation and limits on the server) |
| A4 | Closed, phase 0 |
| D1, D2, D4 | Deferred (envelope encryption, Key Vault) |
| D3 | IT's (host disk encryption on docker-1) |
| D5 | Closed, phase 4 (optional ClamAV: a clamav compose profile; when CLAMAV_HOST is set every upload and every stored drawing is scanned before it is kept, a hit is refused and audited, and an unreachable scanner refuses uploads; off when the host is not set) |
| D6, D7 | Deferred (retention period and customer copy need a decision from Seth and Q.C.) |
| M1 | Closed, phase 2 (append-only audit and packet log written only by the server; every packet write carries both) |
| M2 | Closed, phase 7 (filing seal is HMAC-SHA256 with the server seal key from a Docker secret; a reopen keeps the broken seal and its data book) |
| M3 | Closed, phase 9 |
| M4, M5 | Deferred (logs to stdout now; SIEM shipping is IT's) |
| S1 | Closed, phase 0, tuned phase 10 |
| S2 | Closed, phase 1 |
| S3 | Closed, phase 9 (CI scanning) |
| S4 | Deferred to IT (pentest before go-live) |
| S5 | Closed, phase 2 (React escapes output; server messages are plain text); phase 7 adds escaping in the PDF builders |
| S6 | Closed, phase 7 (the CSV formula guard on every export) |
| S7 | Closed, phase 7 (the controlled-copy stamp on every exported PDF, signed with the seal key; PDF metadata; every export logged in print_log and the audit) |
| S8 | Closed, phase 7 (the PRINTED COPY stamp with "verify revision before use" on every printed page, signed with the seal key; every print logged in print_log and the audit). PLAN.md lists S8 under phase 5, but the print stamps come with the PDFs in phase 7, so S8 closes there. |
| O1, O2 | Closed, phase 9 (pg_dump timer plus PBS; one rehearsed restore); quarterly restore IT's |
| O3, O4 | Closed, phase 10 |
| O5 | Superseded (no migration; M: retired read-only) |
The brief's wording for the identity IDs, which the table above follows:
- I4 and I7 (PIN lockout, PIN signatures verified on the server) are closed by this build.
- I8 is closed for the app's part. The kiosk device lock policy in I8 is IT's, via Intune or GPO.
- I6 (joiner, mover, leaver) is largely carried by the AD groups plus the nightly reconcile. The quarterly access review is still IT's.
- I3 is closed for "PIN checked on the server" and open for "Intune-managed, device certificate". Registered devices use a device cookie for now.
- I1 is partly closed by AD sign-in. There is no SSO yet.
- I5 is superseded. No in-app admin grant exists, so second-admin approval has nothing to approve. Separate IT admin accounts with MFA remain IT's.
Out of scope for this build, with hooks left: writing to VISUAL or M-Files (never), Entra ID sign-in and MFA (I1, I2), envelope encryption with Key Vault (D1, D2), true .xlsx exports, scheduled report emails.