Ownware
Home›Cyresora›Test run
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)

← Back to Cyresora · Manual · Quickstart · API

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →