Cyresora · Test run · run 2026-09-25
Cyresora Test run, as shipped in the download
243 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 with php tests/run.php. Run it yourself after you download.
== 1. Schema install + migrate (SQLite in-memory) == PASS schemaExists() after install PASS table users exists PASS table settings exists PASS table assets exists PASS table vulnerabilities exists PASS table incidents exists PASS table notifications exists PASS table contacts exists PASS table evidence exists PASS table api_keys exists PASS table webhooks exists PASS table invites exists PASS table audit_log exists PASS install() + migrate() are idempotent PASS api_keys.scope exists and defaults to full == 2. The instruments, and when they start applying == PASS CRA Article 14 applies from 11 September 2026 PASS NIS2 transposition deadline PASS three regimes are modelled PASS CRA vulnerability track cites Article 14 PASS NIS2 cites Article 23 PASS four distinct anchors exist PASS ...and they are the ones the instruments name PASS a CRA VULNERABILITY final report is 14 days... PASS ...counted from the corrective measure being available PASS a CRA SEVERE INCIDENT final report is 1 month... PASS ...counted from the submission of the notification PASS the two CRA tracks therefore differ in BOTH duration and anchor PASS NIS2 early warning is 24h from awareness PASS NIS2 notification is 72h from awareness PASS NIS2 final report is 1 month from the SUBMISSION PASS NIS2 has an on-request intermediate report PASS ...and a post-handling final report == 3. 24 and 72 hours are ABSOLUTE durations, computed on UTC == PASS 24 hours PASS 72 hours PASS across a month boundary PASS across a year boundary PASS DST cannot touch a 24-hour deadline (EU clocks go back 2026-10-25) PASS ...nor a 72-hour one PASS hoursBetween is signed PASS ...and negative going backwards == 4. `+1 month` from 31 January — the bug that makes a deadline LATER than the law == PASS PHP overflows 31 Jan + 1 month (this is the hazard) PASS Clock clamps it to the last day of February PASS ...and to the 29th in a leap year PASS 31 March + 1 month is 30 April PASS 31 May + 1 month is 30 June PASS a date that exists in both months is untouched PASS December rolls the year PASS 30 January is fine, 28 February PASS one month across a DST change keeps the local wall-clock time PASS ...which is 00:00 local on both sides of the change PASS 14 days is a calendar span in the business timezone == 5. There is no working-day carve-out, and the register says so == PASS Friday 18:00 + 24h lands on Saturday PASS ...and the register flags it as a weekend deadline PASS Friday + 72h lands on Monday and is not flagged PASS a Sunday is a weekend PASS a Wednesday is not PASS the weekend test is applied in the BUSINESS timezone, not UTC == 6. The ladder: states, and the deadline that has not started == PASS the CRA vulnerability ladder has three stages PASS early warning is due 24h after awareness PASS ...and is still running at hour 10 PASS notification is due 72h after awareness PASS THE FINAL REPORT HAS NO DUE DATE YET PASS ...because its state is pending, not overdue PASS ...and the register names what starts it PASS once a measure is available, the 14 days can be computed PASS ...and the clock is running PASS at hour 30 the 24-hour early warning is overdue PASS ...by six hours, as a negative number PASS the 72-hour notification is still open PASS four hours out, the early warning is due_soon PASS filed before the deadline is submitted PASS filed after it is submitted_late — recorded, not quietly forgiven == 7. The one-month clocks run from the submission instant == PASS the notification was submitted at hour 40 PASS so the final report is due one month from THAT, not from the 72-hour deadline PASS ...which is a different, EARLIER date than anchoring to the deadline would give PASS ...and the difference is exactly the 32 hours they filed early == 8. NIS2 Article 23(4)(c) only exists when the authority asks == PASS unrequested, the intermediate report has no state to act on PASS ...and no due date PASS once requested, it is on the board PASS the post-handling final report waits for the handling to complete PASS worst() surfaces the overdue stage PASS worst() prefers due_soon over open PASS everything filed reads as submitted PASS tones are the kit's own vocabulary PASS a late filing is shown as bad, not as done PASS humanLeft reads in hours under two days PASS ...and in days and hours beyond that PASS ...and says "ago" once it is blown PASS ...and copes with nothing to say == 9. The register, through the real schema == PASS an incident can run two regimes at once PASS ...and gets a ladder for each PASS an unknown regime is dropped rather than crashing PASS a recorded submission anchors the one-month clock through the database too PASS ...and the OTHER regime on the same incident is unaffected by it PASS filing the same stage twice is refused by the database PASS ...but the same stage under a DIFFERENT regime is fine == 10. What the register refuses to record == PASS an incident needs a title PASS ...and a moment of awareness PASS a DATE with no time is refused, because a 24-hour deadline cannot be computed from one PASS an incident with no regime has no clock, so it is refused PASS an unknown regime is refused PASS a good incident passes PASS a local time is converted to UTC for storage PASS ...and back again for the form PASS a UTC install is a no-op == 11. Roles — working an incident is not declaring that a report went out == PASS viewer may NOT incident.edit PASS viewer may NOT notify.submit PASS member may incident.edit PASS member may vuln.edit PASS member may evidence.attach PASS member may NOT notify.submit PASS member may NOT settings.edit PASS responder may notify.submit PASS responder may contact.edit PASS responder may NOT settings.edit PASS admin may notify.submit PASS admin may settings.edit == 12. The documents an authority is handed == PASS a formula is neutralised PASS a plain negative number stays a number PASS ordinary text is untouched PASS the incident report is a real PDF PASS ...whose streams inflate to readable text PASS ...with the key/value block populated (Pdf::kv takes PAIRS, not an associative array) PASS ...naming the incident PASS ...carrying both regime ladders PASS ...stating that the times are UTC PASS ...and warning that the clocks do not pause at a weekend PASS ...and that NIS2 is a Directive whose deadlines are national == 13. Source-level regressions == PASS every route is registered through App::get/App::post (82 routes) PASS no route is registered by a bare function call PASS every routed handler exists PASS every static and Pdf method called exists PASS no handler reaches for a non-existent App::param() PASS Clock never reads the system time (comments stripped before scanning) PASS the calendar-month clamp is present and commented PASS the application dates are pinned, not computed PASS no controller accepts a due date as input PASS every ladder is built by Clock::ladder() PASS Api::requireKey selects the key scope PASS ...and refuses a write on a read-only key at the door PASS Api::mintKey persists the chosen scope PASS the MCP endpoint hands the scope to the core PASS all six MCP tools are declared PASS ...and every one declares a handler the core can call PASS no tool uses a key the core does not read PASS every read-only MCP tool declares readOnly => true PASS the write tools are role-gated PASS a created resource returns 201 through Api::json PASS uploads are sniffed by magic bytes PASS the stored filename is minted PASS the serve path is containment-checked AND re-sniffed PASS nosniff is set on served evidence PASS backup redaction covers password_hash PASS backup redaction covers totp_secret PASS backup redaction covers totp_recovery PASS backup redaction covers token_hash PASS backup redaction covers secret PASS backup redaction covers smtp_pass PASS backup redaction covers backup_token_hash PASS backup redaction covers oidc_client_secret PASS one redaction list, used by both backup doors PASS no view loads anything from an external host PASS no view pulls a font or script from a CDN PASS permanent info alerts are nested so the kit cannot toast them away: contacts.php PASS permanent info alerts are nested so the kit cannot toast them away: dashboard.php PASS the service worker cache is versioned for this release PASS ...and never caches a page navigation PASS the manifest names this product PASS the layout loads the kit and groups the nav PASS the kit itself is untouched PASS the session cookie is named for THIS product PASS no build-environment trace: jay... PASS no build-environment trace: aero... PASS no build-environment trace: eir... PASS no build-environment trace: clean... PASS no build-environment trace: mission... PASS no trace of a spine product: aiac... PASS no trace of a spine product: compl... PASS no trace of a spine product: compl... == 14. The demo board is alive, and shows every state at once == PASS four incidents PASS four products and systems PASS a notification rota with a deputy PASS the board shows an OVERDUE deadline PASS ...a clock still running PASS ...something already submitted PASS ...something submitted LATE PASS ...and a deadline that has NOT STARTED because no corrective measure exists yet PASS the dashboard counts at least one overdue PASS ...and at least one exploited vulnerability with no fix PASS no seeded incident is in the future PASS no seeded notification is in the future PASS no seeded notification precedes its own incident PASS reset clears the register but not the login PASS licence: every font ships the SIL OFL text for its own family beside it PASS licence: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause == F2. The warning setting, and deadline alerts to the rota == PASS warn hours: a 24 h setting marks an early warning 20 h out as due soon PASS warn hours: the default 6 h leaves it running PASS warn hours: the saved setting reaches the board (24 h → due soon, 6 h → running) — it was saved and never read PASS alert points: parsed descending, de-duplicated, nonsense dropped PASS alert points: empty stays empty PASS alert rung: 13 h out reaches no point PASS alert rung: 11 h out is the 12 h point PASS alert rung: 3 h out is the TIGHTEST point reached (4), not every point PASS alert rung: under an hour is the 1 h point PASS alert rung: just overdue is point 0 PASS alert rung: overdue for days alerts no more (switching alerts on does not mail old history) PASS alert rung: a submitted stage never alerts PASS alert rung: a stage whose clock has not started never alerts PASS alert recipients: the regime's notifier and their deputy, nobody else PASS alerts: switched off (the default), nothing is sent PASS alerts: on, a clock 3 h from its deadline fires ONE alert at the 4 h point, to 2 recipients PASS alerts: with no mail server the alert is recorded as no-smtp, never as sent PASS alerts: a second run (cron after a page load) sends nothing — one latch PASS alerts: the Clocks screen is told alerts are on but cannot be delivered PASS alerts: a deadline that has just passed alerts once more (point 0), through the owner's SMTP PASS alerts: a failed send keeps its reason for the Clocks screen PASS alerts: a stage overdue for weeks does not suddenly alert PASS alerts: the email names the stage, the incident, the deadline and says Cyresora files nothing PASS alerts: the email carries no credential or SMTP detail PASS alerts: deadline.alert is a webhook event an endpoint can subscribe to PASS alerts: a cron route runs them, guarded by the backup token PASS alerts: the Clocks screen runs the watcher and shows its health PASS webhooks: notification.recorded fires from the REST route and the agent tool too, after the write PASS webhooks: incident.opened fires from the agent tool too (open_incident), after the write PASS settings: the mail-server copy no longer claims invitations are emailed PASS dashboard: a zero "Due within hours" tile no longer says "act now" PASS backup guard: the harness made one signed-in account per role (viewer,member,responder,admin) PASS backup guard: GET /backup.json — 403 for every non-admin role, open to admin PASS backup guard: GET /backup.sqlite — 403 for every non-admin role, open to admin PASS backup guard: GET /restore — 403 for every non-admin role, open to admin PASS backup guard: POST /restore/dry-run — 403 for every non-admin role, open to admin PASS backup guard: POST /restore/commit — 403 for every non-admin role, open to admin PASS backup guard: POST /restore/token — 403 for every non-admin role, open to admin PASS backup guard: GET /backup/scheduled without its token refuses every signed-in role, admin included PASS backup guard: GET /alerts/run without its token refuses every signed-in role, admin included PASS webhooks: "Add" reaches its route and saves the endpoint (was a 404) PASS webhooks: "Pause" reaches its route and flips the endpoint PASS webhooks: "Delete" reaches its route and removes the endpoint PASS demo card: no page here is public, so every page keeps "signed in as the admin" PASS demo card: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session PASS demo card: boot() hooks it only behind the .demo-mode marker, and a page with no </body> passes through untouched PASS backup redaction: a canary sits in every credential column of the schema (api_keys.token_hash, invites.token_hash, settings.smtp_pass, settings.backup_token_hash, settings.oidc_client_secret, users.password_hash, users.totp_secret, users.totp_recovery, webhooks.secret) PASS backup redaction: each arrives as [REDACTED] and no canary is anywhere in the JSON backup ==================================================== ALL 243 TESTS PASSED (0 failed)