first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-28 09:16:54 +01:00
commit 32a7ba8e8f
168 files changed
+123856

No files matched your search

File diff suppressed because it is too large. Load diff
+82
View File
@@ -0,0 +1,82 @@
# Interactive API documentation — 0.0.1
## URLs
| URL | Content |
| --- | --- |
| `/docs`, `/docs/` | Local Swagger UI with the complete contract and request execution |
| `/openapi.json` | OpenAPI 3.1.0 with 36 operations and input/output models |
| `/redoc` | Compatibility redirect to `/docs` |
The default API base path is `/api`. The server applies the configured `API_PREFIX`
to `servers` and the OAuth2 login URL, and includes the application name and
version in the document metadata. Changing the prefix requires no JSON edits.
## Authentication and examples
In the **Authorize** dialog, select one authentication method:
- `OAuth2PasswordBearer`: the username and password of an existing account. The
documentation submits a form to `/api/auth/login` and uses the returned token.
- `BearerAuth`: the `access_token` value, without the `Bearer` prefix.
For an operation, select **Try it out**, fill in the fields, and select **Execute**.
Requests perform real operations on the current instance. Updates, outgoing
messages, backups, and deletions use the same permissions and limits as the
application interface. Registration requires an explicit `ALLOW_REGISTRATION=true`.
Add a SwitchOS device using the global username/password pair:
```json
{
"name": "switch-01",
"device_type": "switchos",
"host": "192.0.2.10",
"port": 80,
"use_global_switchos_credentials": true
}
```
Update only the binary backup schedule:
```json
{
"binary_cron": "0 2 * * *"
}
```
Omitted fields remain unchanged. Empty or null secret fields preserve stored
values; explicit `clear_*` flags remove them. Public responses do not expose stored
passwords or keys. A separate endpoint reveals the global SSH key after the
account password is entered again.
## Assets and maintenance
`docs/openapi.json` is the contract template. `src/api/documentation.rs` serves it
with the current instance configuration. The HTML, initializer, stylesheet, and
local Swagger UI distribution are embedded directly from the checked-in source
files at compile time. On first use, the application combines the embedded assets
into one `docs.js` and one `docs.css`. Their URLs contain content hashes for browser
caching. Documentation assets do not depend on generated files in `OUT_DIR`;
`build.rs` builds only the main application assets.
The official `swagger-ui-dist` **5.34.0** distribution is pinned and was downloaded
from its publisher registry. The archive SHA-512 was checked against the published
integrity value. Original assets, the Apache-2.0 license, NOTICE, and bundled
library licenses are in `vendor/swagger-ui/`. Source details and individual file
checksums are recorded in `vendor/swagger-ui/PROVENANCE.json`.
Upstream sources: [Swagger UI installation](https://github.com/swagger-api/swagger-ui/blob/main/docs/usage/installation.md),
[configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/),
[swagger-ui-dist package](https://registry.npmjs.org/swagger-ui-dist/5.34.0).
The remote validator, query-string configuration, and persistent browser token
storage are disabled. The request interceptor permits only same-origin requests;
the Content Security Policy disallows external scripts. Building and running the
documentation requires neither Node nor a CDN connection.
`tests/openapi_contract.rs` checks every documented method and path against the
actual Axum router, authentication requirements, local schema references, a custom
API prefix, English documentation, and embedded assets. It also checks that the
cache keys in the HTML match the served asset contents. Browser testing covers
rendering and request execution on an isolated instance.
+24
View File
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>MikroTik Backup System — API documentation</title>
<link rel="stylesheet" href="/assets/docs.css">
<script src="/assets/docs.js" defer></script>
</head>
<body>
<header class="docs-header">
<a href="/">MikroTik Backup System</a>
<nav aria-label="Documentation"><a href="/openapi.json" download="mikrotik-backup-openapi.json">OpenAPI JSON</a><a
href="/">Back to the application</a></nav>
</header>
<main id="swagger-ui" aria-label="API documentation"></main>
<noscript>Enable JavaScript to use the interactive documentation. The JSON specification is available at
/openapi.json.</noscript>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
(function () {
'use strict';
window.apiDocs = SwaggerUIBundle({
url: '/openapi.json',
dom_id: '#swagger-ui',
deepLinking: true,
displayRequestDuration: true,
docExpansion: 'list',
filter: true,
defaultModelsExpandDepth: 0,
persistAuthorization: false,
validatorUrl: null,
queryConfigEnabled: false,
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
requestInterceptor: function (request) {
if (new URL(request.url, location.href).origin !== location.origin) {
throw new Error('API documentation can send requests only to this instance.');
}
return request;
}
});
window.apiDocs.initOAuth({ clientId: 'swagger-ui', appName: 'MikroTik Backup System' });
}());
+92
View File
@@ -0,0 +1,92 @@
html {
background: #fafafa;
color: #1b2734;
color-scheme: light
}
body {
margin: 0;
font-family: system-ui, sans-serif
}
.docs-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 17px 24px;
background: #101e2b;
color: #e4edf5
}
.docs-header a {
color: inherit;
text-decoration: none;
font-size: 14px
}
.docs-header>a {
font-weight: 600
}
.docs-header nav {
display: flex;
gap: 22px
}
.docs-header a:focus-visible {
outline: 2px solid #36b8f4;
outline-offset: 5px
}
.docs-header a:hover {
text-decoration: underline
}
.swagger-ui .info {
margin: 30px 0
}
.swagger-ui .wrapper {
max-width: 1440px
}
.swagger-ui .info .description p {
line-height: 1.6
}
.swagger-ui .scheme-container {
box-shadow: none;
border-top: 1px solid #dce1e6;
border-bottom: 1px solid #dce1e6
}
noscript {
display: block;
padding: 24px
}
@media(max-width:640px) {
.docs-header {
align-items: flex-start;
flex-direction: column;
gap: 12px;
padding: 16px
}
.docs-header nav {
gap: 18px
}
.docs-header a {
font-size: 12px
}
.swagger-ui .info .title {
font-size: 26px
}
.swagger-ui .wrapper {
padding: 0 12px
}
}
+134
View File
@@ -0,0 +1,134 @@
# Architecture — release 0.0.1
## One process with explicit module boundaries
Axum serves the HTTP API and embedded interface assets. Tokio manages requests,
scheduling, and notification tasks. Local SQLite access uses `rusqlite`; a mutex
protects a single connection configured with foreign keys, WAL, and a busy timeout.
The SSH library is synchronous, so blocking sessions run through `spawn_blocking`.
The application does not launch Python, Node, `ssh`, `scp`, `ping`, or system cron
subprocesses.
Interface sources remain separated by responsibility. `web/assets.list` specifies
concatenation order. `build.rs` produces one `app.js`, one `app.css`, and the HTML in
Cargo's `OUT_DIR`; the compiler embeds them in the executable. Deployment does not
read `web/` from disk or fetch UI assets from a CDN. A source change requires a new
build. The distribution contains source only, and `start.sh` builds before launching.
CSS and JavaScript URLs include content-derived identifiers so browsers fetch changed
assets after an update without requiring manual cache clearing.
The same process serves `/docs` and `/openapi.json`. The API documentation module
embeds checked-in files directly with `include_str!`, combines the local Swagger UI
and integration assets into one CSS and one JavaScript response, and supplies
content-derived cache identifiers. This path does not depend on generated
`OUT_DIR/docs.*` files. Documentation assets load only on the documentation page.
The specification covers 36 operations and uses the configured API prefix. Assets
and validators are not fetched from external services. See
[API documentation integration](api-documentation.md).
## Operations and data consistency
Requests validate an HS256 JWT, the current user's existence, and device/backup
ownership. Password verification and hashing have a separate four-operation pool;
saturation returns 429 without blocking the rest of the API. Login throttling also
limits incorrect password attempts. Global settings and logs retain the source
application's scope: authenticated operators.
Reading a complete request body has a 1 MiB limit and a 15-second deadline. An
incomplete request cannot indefinitely prevent shutdown. This deadline does not
limit the execution time of a backup operation.
A backup acquires a device lock and a slot in the operation pool. A tracked task owns
the lock, so a disconnected HTTP client cannot release it halfway through a transfer.
Data is written to a private temporary file under `DATA_DIR`. After the transfer,
the application validates size, synchronizes the file, calculates SHA-256, and
publishes it. The SQLite record is committed last; a database failure removes the
published file. Deletion stages the file under a temporary name and compensates if
the database update fails.
Each backup access checks `DATA_DIR` boundaries, names, and symlinks. Bulk ZIP creation
has an aggregate size limit and uses a temporary file; the response streams that
file instead of holding the complete archive in memory. The data directory must
remain private: protection against a local user concurrently changing files depends
on preventing shared write access to it.
## Protocols and error handling
RouterOS operations use SSH, pinned host keys, password/private-key authentication,
`/export`, `/system backup save`, and SFTP with an SCP fallback. Device-side temporary
files are removed after transfer. Upload stops after writing the file; it never
issues a restore command. The default `accept-new` policy records the first observed
host key and requires the same key later. Explicit `strict` requires an already
trusted entry. Host identity keys and client authentication keys serve different
purposes. A timeout covers the entire session; a separate timer closes the socket
if the peer stalls an operation.
SwitchOS uses HTTP(S), Digest or Basic authentication, response validation, and a
size limit. Actual HTML is rejected as a backup regardless of its MIME type, while
otherwise nonempty firmware-specific `.swb` content is not rejected by MIME/signature
heuristics. A public HTTP 200 page does not prove backup authentication. When needed,
the adapter obtains a Digest challenge from `/sys.b` and signs the actual
`/backup.swb` request URI. Connection tests report web reachability separately from
backup availability. Bounded same-host redirects are followed; redirects to a
different host or an HTTPS-to-HTTP downgrade are stopped.
SwitchOS credentials have an explicit global or device-local source. Imported rows
without a selection preserve their original field-by-field fallback. Selecting a
source does not erase the other stored credentials. The global mode reads current
settings on each connection. See [protocol details and sources](switchos.md).
ICMP uses a native socket; missing socket permissions produce an explicit diagnostic
error. Device connections are initiated only to hosts configured by operators.
Known secrets are redacted from public responses and logs. Notification failures do
not invalidate a successfully stored backup. Batch operations return a result for
each device, including failures and skips.
## Scheduling and shutdown
The scheduler reads settings once per second, evaluates five-field cron expressions
in the configured timezone, and prevents overlapping instances of the same job.
Numeric weekdays preserve APScheduler semantics: Monday is 0. Export and binary
jobs respect device policy flags. After a restart, the next execution is calculated;
the application does not replay a complete queue of jobs missed while it was off.
A delayed tick tolerates a scheduling delay of up to five minutes.
`SIGTERM` and `Ctrl+C` stop accepting HTTP requests, prevent new pooled operations,
stop scheduling, and wait for operations already in progress. A WAL checkpoint runs
before exit. The process holds `application.lock`; the explicit importer uses the
same storage lock. Deployments should allow in-progress operations to finish.
Compose and systemd use a 300-second stop timeout by default; increase this if
operation timeouts or the number of devices require a longer shutdown window.
## Fresh initialization and explicit import
`src/schema.sql` contains the complete current schema. If the database file does not
exist, startup creates a fresh database. If a database file exists, startup first
opens it read-only and validates that it already has the supported schema. An older
or unrelated schema is rejected with instructions to use a fresh installation and
explicit import. Startup does not run `ALTER TABLE`, create upgrade snapshots, add
migration journals, or move backup files.
The separate `import-python --source DIR [--database PATH] [--backup-root DIR]`
command imports into a new destination. The `import-python.sh` wrapper builds the
application and invokes that command. Import opens the source read-only, preserves
IDs, ownership, password hashes, settings, stored credentials, and operation logs,
and copies validated backup content to the destination. Existing destination
databases or files are not overwritten. See the full
[Python import procedure](data-import.md).
`DATA_DIR` contains the database by default, backups, the JWT secret, trusted host
keys, and locks. A separately configured `DATABASE_PATH` must also be included in
operational backups. These files are deployment data, not release source. Persistent
changes should go through the application; a consistent copy of an active database
requires the SQLite Online Backup API or stopping the service first.
Binary files use `backups/binary/<router_id>/`, and text exports use
`backups/exports/<router_id>/`. Stable device IDs keep paths unchanged when devices
are renamed. Fresh backups and explicitly imported files use this layout; startup
does not convert an older layout automatically.
`Cargo.lock`, the pinned toolchain, the schema/import implementation, and documented
libssh2 backports are part of the release. [The port matrix](port-matrix.md) identifies
both source archives by SHA-256. Dependency checks must be repeated for future
releases; an audit result is evidence for a specific dependency set and date, not a
claim that every possible defect has been excluded.
+262
View File
@@ -0,0 +1,262 @@
# Fresh installation and explicit Python import
Version **0.0.1** starts with a clean installation. It does not update an old
schema or move existing backup files during startup. Importing data from the
Python/TypeScript application is a separate, explicit operation.
## Choose one installation path
For an empty installation, configure the new application's `.env` and run:
```sh
./start.sh
```
A database is initialized only when the selected database file does not exist.
The complete current schema is in `src/schema.sql`. The initial account uses
`DEFAULT_ADMIN_USERNAME` and `DEFAULT_ADMIN_PASSWORD`; if the password is omitted,
a generated value is stored in `DATA_DIR/secrets/initial-admin-password`.
Registration remains controlled by the new application's configuration.
If you intend to import Python data, **run the import before starting the new
server**. Starting the server first creates a database, and the importer will
refuse to overwrite or merge it. Use a different new destination if a database
already exists; do not delete a database merely to bypass this check.
## Startup never upgrades an existing database
Startup accepts an existing database only when it has the current `schema_info`
marker, supported format version, required tables and columns, valid integrity,
and the expected user/settings records. An older Python schema, an earlier Rust
work-in-progress schema, an unknown version, or a pre-existing empty database file
is rejected with instructions to select a fresh destination or use the explicit
Python importer. No `ALTER TABLE`, persistent pre-upgrade snapshot, migration
journal, automatic import, or backup relocation is performed.
Before creating storage locks or secrets, the server performs a read-only
preflight. SQLite inspects a private temporary copy of the database and its WAL
or rollback journal, so opening an incompatible database cannot create a source
`-shm` file. The temporary inspection copy is removed afterwards. The schema is
checked again while the server holds its storage locks. Existing current records
and file paths are left unchanged.
## Import command
Stop the old Python application, workers, and schedules. Keep its database, WAL,
rollback journal, and backup files together. Make an independent copy of the old
installation before switching services. This is an offline import, not a live
replication mechanism.
Use the helper from the new Rust project:
```sh
./import-python.sh /srv/old-mikrotik-app
```
Its equivalent application command is:
```sh
./start.sh import-python --source /srv/old-mikrotik-app
```
Optional source overrides are available when paths are customized or discovery
finds more than one candidate:
```sh
./import-python.sh /srv/old-mikrotik-app \
--database docker-data/db/routeros_backup_next.db \
--backup-root docker-data
```
Both `--database` and `--backup-root` are resolved **relative to `--source`**, unless
an absolute path is supplied. They select the old source, not the destination.
The helper resolves its first directory argument relative to the caller's working
directory before invoking the project's starter.
The destination comes from the new application's configuration. An explicit
example is:
```sh
DATA_DIR="$PWD/storage-imported" \
DATABASE_PATH="$PWD/storage-imported/routeros_backup_next.db" \
./import-python.sh /srv/old-mikrotik-app \
--database docker-data/db/routeros_backup_next.db \
--backup-root docker-data
```
After a successful import, use the same destination to start the server:
```sh
DATA_DIR="$PWD/storage-imported" \
DATABASE_PATH="$PWD/storage-imported/routeros_backup_next.db" \
./start.sh serve
```
The `migrate` command from previous revisions has been removed. The importer does
not start HTTP or background schedules. It does not add a new administrator when
users have been imported. Log in with an imported account and its existing password.
## Source discovery
Discovery follows the layouts present in the supplied Python/TypeScript project:
| Source layout | Recognized database location |
| --- | --- |
| Shipped Compose bind mount `./docker-data:/app/storage` | `docker-data/db/routeros_backup_next.db` or `docker-data/routeros_backup_next.db` |
| Local application working directory | `storage/routeros_backup_next.db` |
| Local backend working directory | `backend/storage/routeros_backup_next.db` |
| Explicit source configuration | SQLite `DATABASE_URL` and `DATA_DIR` in `.env` or `backend/.env` |
The known data directories are also checked for the documented `db/` database
subdirectory. Container paths under `/app/storage` are mapped to the shipped
`docker-data` mount. A customized Compose volume mapping is not executed or
interpreted as arbitrary code; supply explicit paths when it differs.
Only actual existing candidates are considered. **Multiple different database
files require `--database`**; discovery never chooses the first, newest, or largest
file. A configured database or data directory that is unavailable produces an
error instead of silently substituting another installation. Source overrides
allow the operator to resolve that situation explicitly.
The importer reads only `DATABASE_URL` and `DATA_DIR` from source environment
files. It does not execute shell expressions, expand variables, import the Python
application, or copy the source `.env`. Dynamic expressions and unsupported SQLite
URL options require explicit source paths. Providing both overrides bypasses
source environment-path discovery.
For each backup record, the importer resolves the recorded path or its preserved
filename within the selected or recognized source data roots. It supports the
original flat layout and older device-name subdirectories. Missing files,
multiple distinct matches, parent traversal, and symlinks escaping a selected
root are errors. Choose an unambiguous `--backup-root` when necessary; no source
file is silently skipped.
## What is preserved
The import creates a new database using the current schema and copies supported
records logically from a private source snapshot. It preserves:
- User, device, backup, settings, and operation-log IDs; device ownership.
- Existing password hashes, usernames, preferences, timestamps, and backup metadata.
- RouterOS/SwitchOS connection information, per-device credentials, and backup policies.
- Global settings, schedules, SSH keys, SwitchOS credentials, SMTP settings/secrets,
and Pushover settings/secrets stored in SQLite.
- Every referenced backup's bytes, original `file_name`, and existing checksum.
Fields absent in an older supported Python schema receive current defaults in the
new database only. A missing old `file_name` is derived from its recorded path.
Missing/null historical timestamps use the current schema's default when that
older schema did not store a usable value. A missing password hash, orphaned
ownership, or unsupported source structure causes an error; import never assigns
an arbitrary owner or replaces a missing password with an administrator password.
An empty source settings table receives the normal default settings row.
Imported SwitchOS records have `use_global_switchos_credentials = null` when the
Python schema has no explicit source selector. This preserves the former per-field
inheritance. Choosing global or device credentials later uses a complete pair
from that source without deleting saved device credentials. Secrets are not
returned in the public API or prefilled into the interface.
Passlib PBKDF2-SHA256 and supported Werkzeug PBKDF2 hashes remain usable without a
reset. New and changed passwords use Argon2id. The source environment's JWT secret
is not copied. Unless `SECRET_KEY` is explicitly configured for the new server,
its first startup creates a new `DATA_DIR/secrets/jwt.key`, and old sessions must
log in again. The import itself does not generate a JWT key.
## Destination layout and publication
The destination must be separate from the source application and source backup
roots. Its database must not exist, and its `backups/` tree must contain no existing
files. Existing empty directories, the new configuration's private files, and
storage locks do not authorize overwriting a database or backup. Relative paths,
parent components, and existing symlinks are resolved before any directory or lock
is created, using the same normalized paths for validation and subsequent writes.
Imported and newly created files use stable numeric device IDs:
| Backup type | Directory relative to `DATA_DIR` |
| --- | --- |
| RouterOS or SwitchOS binary | `backups/binary/<device-id>/` |
| RouterOS text export | `backups/exports/<device-id>/` |
An imported physical filename receives the backup ID as a prefix. Its metadata
`file_name` remains unchanged for downloads and RouterOS uploads. Names are bounded
and published without replacing existing files or symlinks. Renaming a device does
not rename its numeric directory. Database paths to these files are relative to
`DATA_DIR`.
The importer reads the stopped source database and any `-wal` or `-journal` as
ordinary files, copying them into private staging. It compares metadata and SHA-256
before and after copying and refuses a detected change. SQLite opens only this
private copy and includes committed WAL records. The source `-shm` is neither opened
by SQLite nor recreated; staging reconstructs its own coordination files when
needed. Keep the source stopped throughout the operation, because these checks
are not a substitute for coordinating concurrent writers.
All source rows and backup files are validated before the target database is
published. Every copied file is checked against its recorded SHA-256 when present
and against the copy's calculated SHA-256. Publication uses private temporary
files, file synchronization, atomic no-clobber publication, and directory
synchronization. The destination database is published last.
Before database publication, error cleanup makes a best-effort attempt to remove
files recorded as installed by that attempt; source files remain unchanged. If
directory synchronization fails after database publication, the database and its
referenced backups are retained, and the error asks the operator to inspect the
destination. An interrupted process or filesystem I/O failure can leave staging
or unreferenced files: inspect or choose
another new destination instead of expecting automatic startup recovery. There is
no migration journal or automatic resumption framework.
The CLI prints a JSON summary with the selected source database and counts of
users, devices, backup records, copied files, and logs. It never prints saved
passwords or keys. Check that report, then start the new service, inspect device
lists, preview an export, and download representative binary and text files.
Do not run both installations' schedules for the same devices during cutover.
## Rollback and operational limits
The original installation remains the rollback source. To return to it, stop the
new service and restart the old application on its unchanged data. New backups
created after cutover exist only in the new installation; preserve them and its
database before discarding or reverting anything. Do not exchange a SQLite main
file under a running process or mix unrelated main/WAL/journal files.
The importer does not copy operating-system permissions, service definitions,
external environment secrets, or SSH host-trust files. Review the new environment
configuration and host-key policy before first device contact. Imported SQLite
secrets retain their original representation; target database/secret files use
private permissions, and deployment storage must protect their contents.
Regression tests cover fresh initialization/restart, rejection without source
changes, explicit path selection and ambiguity, record/secret preservation, new
file layout, checksums, locks, symlinks, committed WAL data, and source-sidecar
immutability. Tests use generated fixtures; no production database or physical
MikroTik was supplied. Current verification results are recorded separately in
[validation.md](validation.md).
## Import with Docker Compose only
If the host does not have a Rust toolchain, run the importer from the same image as
the server. Stop the old application and make sure the new application's target
volume does not already contain a database. Mount the legacy project read-only:
```sh
docker compose build app
docker compose run --rm \
-v /srv/old-mikrotik-app:/legacy:ro \
app import-python \
--source /legacy \
--database docker-data/db/routeros_backup_next.db \
--backup-root docker-data
```
After the importer prints a successful JSON report, start the service:
```sh
docker compose up -d
```
If the legacy directory uses a different layout, change only the source paths passed
to `--database` and `--backup-root`. The target data remains in the new `/data`
volume.
+21
View File
@@ -0,0 +1,21 @@
# Importing data from the Python/TypeScript release
The current release does not perform automatic data migration and does not expose the
old `migrate` command. The supported path is the explicit, copy-only
`import-python` command. See [data-import.md](data-import.md) for the full source
discovery, validation, rollback, and failure rules.
For the standard layout of the supplied legacy project:
```sh
DATA_DIR="$PWD/storage-imported" \
DATABASE_PATH="$PWD/storage-imported/routeros_backup_next.db" \
./import-python.sh /srv/old-mikrotik-app \
--database docker-data/db/routeros_backup_next.db \
--backup-root docker-data
```
Stop the old application first. The destination directory must be new and must not
contain an existing database. The import preserves application records and copies
referenced backup files. It intentionally does not copy external environment secrets,
service-manager files, operating-system permissions, or external SSH host-trust files.
+14
View File
@@ -0,0 +1,14 @@
{
"checked_at_utc": "2026-10-27T21:39:43.856005+00:00",
"tool": "cargo-audit 0.22.2",
"lockfile": "Cargo.lock",
"lockfile_sha256": "341839930e9b231b548dbd1dbbc8e497531ff9c4903b3cf07722f3d9f024e2d8",
"command": "cargo audit --file Cargo.lock --json",
"advisory_database": {
"advisory-count": 1380,
"last-commit": "99670cfc0bc574433034b75876a4b791ccaa2efb",
"last-updated": "2026-10-27T11:02:42+01:00"
},
"report_sha256": "49e44f55fdaf3e2aacd358d312489882a91147f8790a81348ffa3f9fd76e6634",
"note": "Retained advisory scan metadata. No ignored advisories; vendored C backports are summarized in security.md."
}
+1
View File
@@ -0,0 +1 @@
{"database":{"advisory-count":1380,"last-commit":"99670cfc0bc574433034b75876a4b791ccaa2efb","last-updated":"2026-10-27T11:02:42+01:00"},"lockfile":{"dependency-count":270},"settings":{"target_arch":[],"target_os":[],"severity":null,"ignore":[],"informational_warnings":["unmaintained","unsound","notice"]},"vulnerabilities":{"found":false,"count":0,"list":[]},"warnings":{}}
+112
View File
@@ -0,0 +1,112 @@
# Dependency review — release 0.0.1
The audit documented here was performed on **27 October 2026 for source revision 2**
of release 0.0.1. Its scope was `Cargo.lock`, known public advisories, and the C SSH
library bundled through `libssh2-sys`. Revision 3 additionally verified the provenance
of local Swagger UI assets. These historical checks are not a claim that a new
advisory scan has been run for revision 4; see [the validation report](validation.md).
A scanner result is limited to its advisory database and examined versions and does
not establish the absence of every vulnerability.
## RustSec and dependency changes
The recorded run used **cargo-audit 0.22.2** and a RustSec database containing
**1,380 advisories**, at commit `99670cfc0bc574433034b75876a4b791ccaa2efb`, last updated
`2026-10-27T11:02:42+01:00`. The audited dependency set produced **0 reported
vulnerabilities and 0 warnings**. The complete JSON result is preserved in
[`dependency-audit.json`](dependency-audit.json); the lockfile identity is recorded in
[`dependency-audit-metadata.json`](dependency-audit-metadata.json).
The original final revision 2 scan JSON is unchanged. Its `last-commit` and
`last-updated` fields contain the database revision and timestamp above. Metadata
records the audit time, tool version, report SHA-256, and the audited `Cargo.lock`
SHA-256: `341839930e9b231b548dbd1dbbc8e497531ff9c4903b3cf07722f3d9f024e2d8`.
To repeat the audit:
```sh
cargo audit --file Cargo.lock --json
```
No `--ignore` exceptions were used. The scan did not modify the manifest or lockfile.
| Dependency | Release version | Reason |
| --- | --- | --- |
| `zip` | 9.1.2 | Replaced 2.6.1, affected by RUSTSEC-2026-0367, -0368, and -0369. The application creates archives rather than invoking the affected extraction functions, but the release still uses the corrected library. |
| `jsonwebtoken` | 11.1.0 with `aws_lc_rs` | Replaced 9.3.1, affected by CVE-2026-25537. Corrected validation rejects invalid date types; additional application tests cover `exp`, `nbf`, algorithm, signature, and compatibility with Python HS256 tokens. |
| `tokio` | 1.54.1 | Includes fixes for RUSTSEC-2026-0384 and RUSTSEC-2026-0386. |
| `rustls-webpki` | 0.103.16 | Uses the corrected version assessed by the recorded RustSec database. |
| `ssh2` / `libssh2-sys` | 0.9.6 / locally patched 0.3.3 | Adds missing fixes to the bundled C code, described below. |
Sources: [RustSec 0367](https://rustsec.org/advisories/RUSTSEC-2026-0367.html),
[0368](https://rustsec.org/advisories/RUSTSEC-2026-0368.html),
[0369](https://rustsec.org/advisories/RUSTSEC-2026-0369.html),
[jsonwebtoken maintainer advisory](https://github.com/Keats/jsonwebtoken/security/advisories/GHSA-h395-gr6q-cpjc),
[RustSec 0384](https://rustsec.org/advisories/RUSTSEC-2026-0384.html), and
[0386](https://rustsec.org/advisories/RUSTSEC-2026-0386.html).
## Local API documentation assets — revision 3
Swagger UI comes from the pinned official `swagger-ui-dist 5.34.0` package. The
archive's integrity was checked against its published npm SHA-512 value. The retained
files' SHA-256 values and source are recorded in `vendor/swagger-ui/PROVENANCE.json`.
The license, NOTICE, and embedded-library license information are retained. Neither
the application nor the browser fetches these assets from a CDN.
The documentation loads only its local specification. The online validator,
query-string configuration, and persistent authorization are disabled. A same-origin
request interceptor and CSP restrict requests. The RustSec audit above covers Rust
dependencies; it is not a JavaScript security scan of this package. See
[API documentation integration](api-documentation.md) for provenance and behavior.
Revision 4 changes how these checked-in files are embedded, without replacing the
vendored Swagger UI files.
## Bundled libssh2 fixes
A current crate version alone was insufficient. Published `libssh2-sys 0.3.3`
contained a libssh2 1.11.1 base with four earlier backports, but omitted later fixes.
Specific functions were compared against upstream libssh2 patches. This confirmed
missing AES-GCM bounds checks, short ETM packet checks, and the other corrections
listed below. The application uses the affected encryption negotiation and SFTP
code, so the release includes code fixes rather than only documenting the findings.
`vendor/libssh2-sys` contains the original sources needed for compilation, original
licenses, preserved upstream patches, their SHA-256 values, and a patch reproducing
the changes. Cargo uses this local dependency. Compilation always uses the patched
code; it does not silently substitute a system libssh2.
| Advisory | Fix scope | Upstream source |
| --- | --- | --- |
| CVE-2026-7598 | Username and authentication data lengths | [256d04b60d80](https://github.com/libssh2/libssh2/commit/256d04b60d80bf1190e96b0ad1e91b2174d744b1) |
| CVE-2026-66032 | Double-free of an SFTP file-open response | [5e4776146552](https://github.com/libssh2/libssh2/commit/5e4776146552d898b9c0e1b313cd093fa8dc92d0) |
| CVE-2026-66033 | AES-GCM buffer bounds and release-build length calculation | [a2ed82d40964](https://github.com/libssh2/libssh2/commit/a2ed82d40964bbc0d64cd717aa0a5a892117d2e6) |
| CVE-2026-66034 | Comment length in a publickey subsystem response | [a13bb6c773f0](https://github.com/libssh2/libssh2/commit/a13bb6c773f0d55ad1628cede57e99803cd898d9) |
| CVE-2026-66035 | Minimum ETM packet length before decryption | [42e33d81577e](https://github.com/libssh2/libssh2/commit/42e33d81577ed4b95d4b4f6f845e5ee8efe5eeb4) |
| CVE-2026-58050 | Publickey attribute limit before allocation | [34497525929b](https://github.com/libssh2/libssh2/commit/34497525929b9a47f03dfb81887ac896202b7e12) |
| CVE-2026-58051 | Publickey list entry initialization before cleanup | [a9758da45a52](https://github.com/libssh2/libssh2/commit/a9758da45a52bc8c630ec9493804d0c6ea30b24a) |
Two upstream follow-up fixes for freeing partially created attributes and initializing
the list tail are also included. Full commit IDs, private-function name adaptations,
and reproduction instructions are in
[`PROVENANCE.md`](../vendor/libssh2-sys/PROVENANCE.md). Distribution notices
[USN-8722-1](https://ubuntu.com/security/notices/USN-8722-1) and
[USN-8799-1](https://ubuntu.com/security/notices/USN-8799-1) independently describe
these advisory families.
## Verification and limits
The patch was reproduced on a fresh copy of the published crate; all seven modified
files matched the bundled code byte for byte. `vendor/libssh2-sys/SHA256SUMS` supports
integrity checks of the included files.
A separate C regression program in the source tree invokes the same static library
with invalid AES-GCM lengths and overflowing username lengths. Its execution
instructions are in `PROVENANCE.md`. Actual build, Rust-test, C-test, and SSH-session
results are recorded by revision in [the validation report](validation.md).
RustSec checks Rust dependency identities and versions. It does not replace C code
review, container image scanning, or updates to system OpenSSL and zlib libraries.
Local SSH-server tests exercise the real protocol, but do not reproduce every attack
family or establish compatibility with every physical MikroTik model. Dependency
updates require a new scan, relevant tests, and comparison of these backports with
the newer upstream release.
+105
View File
@@ -0,0 +1,105 @@
# Import from the previous Python/TypeScript application
The Rust service does not perform automatic data migrations on startup. Use the explicit `import-python` command once, with the previous application stopped.
## What is imported
The importer copies supported data from:
- `users`;
- `routers`;
- `backups`;
- `operation_logs`;
- `global_settings`.
Existing IDs, password hashes, credentials, timestamps, settings, and saved language/font preferences are retained when represented by the current schema.
Every backup record must resolve to exactly one physical source file. Referenced `.rsc`, RouterOS binary backups, and SwOS backup files are copied into the new layout. Existing SHA-256 values are verified when present. The resulting report includes both `backups` and `files_copied`; for a complete import they are equal.
Files that exist in an old backup directory but have **no record in the old `backups` table** are not imported.
## Before importing
1. Stop the old application so its SQLite database and WAL/journal files are stable.
2. Choose a new/empty Rust `DATA_DIR` or `DATABASE_PATH`.
3. Keep the source installation read-only if possible.
The importer refuses to merge into an existing destination database and refuses a destination inside the source tree.
## Recommended command
```bash
./import-python.sh /srv/mikrotik-backup-old
```
Equivalent direct command after building:
```bash
./target/release/mikrotik-backup import-python \
--source /srv/mikrotik-backup-old
```
Docker Compose:
```bash
docker compose run --rm \
-v /srv/mikrotik-backup-old:/legacy:ro \
app import-python --source /legacy
```
Use the same destination volume/environment later with `docker compose up -d`.
## Source discovery
The importer reads plain path values from `.env` or `backend/.env` when available and also checks common paths from the previous application:
```text
docker-data/routeros_backup_next.db
docker-data/db/routeros_backup_next.db
storage/routeros_backup_next.db
storage/db/routeros_backup_next.db
backend/storage/routeros_backup_next.db
backend/storage/db/routeros_backup_next.db
```
If multiple databases exist, select one explicitly:
```bash
./import-python.sh /srv/mikrotik-backup-old \
--database docker-data/db/routeros_backup_next.db
```
If backup files live outside the detected data directories:
```bash
./import-python.sh /srv/mikrotik-backup-old \
--backup-root /absolute/path/to/old/backups
```
Paths supplied to `--database` and `--backup-root` may be absolute or relative to `--source`.
## Failure behavior
Import stops without publishing the new database when, for example:
- the source database is missing, ambiguous, corrupt, or is already a Rust database;
- a required source table/field is missing;
- a backup record has no matching file;
- more than one candidate file matches a backup record;
- a stored checksum does not match the source file;
- the destination database already exists.
The original source is never modified. The source SQLite database is first copied to private staging storage before records are read.
## After import
Check the JSON report. In particular:
```json
{
"backups": 152,
"files_copied": 152
}
```
Then start the Rust application with the same destination configuration and verify at least one device connection, one text export, one binary/SwOS backup, and file download.
+7246
View File
File diff suppressed because it is too large. Load diff
+222
View File
@@ -0,0 +1,222 @@
# Port coverage matrix — 0.0.1
## Scope and source identity
The supplied source code, rather than the interface concept alone, defines functional
coverage. The archive contains no `.git` metadata, so the supplied archive SHA-256
values identify the reviewed inputs:
| Archive | SHA-256 |
| --- | --- |
| `mikrotik_backup_system-master.zip` | `3be24e72b1816070a78ec7ee7336ae0a7f884db070dc019a8afdd9c229c05f47` |
| `mikrotik_backup_system_rust_migration.zip` | `65e4ee36cd60e81e4ff25a6d12d5fd7913b9360b61c96b7301b3de550c00a104` |
The source has **36 active API method/path combinations**, all represented in the
Axum router. Paths below use the default `/api` prefix; `API_PREFIX` remains
configurable.
The test column identifies source tests and their intended coverage. A test's
presence is not proof of a successful run. The revision-specific
[validation report](validation.md) records actual test, interface, build, and process
checks. Controlled fixture servers exercise protocols without contacting production
devices. No physical MikroTik or live Pushover account was supplied.
## Contract test key
These test names are in `tests/api_contract.rs`.
| Key | Full test name |
| --- | --- |
| A | `auth_json_form_preferences_password_and_expiry` |
| R | `registration_and_login_rate_limit` |
| D | `devices_defaults_validation_secrets_and_disabled_operations` |
| O | `ownership_applies_to_every_device_and_backup_operation` |
| F | `files_filters_preview_diff_download_zip_and_bulk_deletion` |
| S | `settings_secrets_validation_reveal_scheduler_and_notification_errors` |
| L | `logs_retention_and_embedded_web_do_not_expose_storage` |
## API operations
| Method and path | Rust file / handler | Test and coverage |
| --- | --- | --- |
| `POST /api/auth/register` | `src/api/auth.rs` / `register` | R: registration disabled/enabled, conflicts, validation |
| `POST /api/auth/login` | `src/api/auth.rs` / `login` | A: JSON and form; R: failed-login throttling |
| `GET /api/auth/me` | `src/api/auth.rs` / `me` | A: account, missing/invalid token, no password hash in JSON |
| `PUT /api/auth/preferences` | `src/api/auth.rs` / `preferences` | A: four languages, font, unsupported values |
| `POST /api/auth/change-password` | `src/api/auth.rs` / `change_password` | A: current password, new login, rejection of old password |
| `GET /api/dashboard` | `src/api/system.rs` / `dashboard` | O: owner-scoped counts; F: counts updated after deletion |
| `GET /api/routers` | `src/api/devices.rs` / `list` | O: user list isolation |
| `GET /api/routers/ping-statuses` | `src/api/devices.rs` / `ping` | D: disabled flag; ICMP tests in `routeros.rs` |
| `POST /api/routers` | `src/api/devices.rs` / `create` | D: RouterOS/SwOS, default ports, names, secrets |
| `GET /api/routers/{id}` | `src/api/devices.rs` / `get_one` | D: metadata and flags; O: another user's record |
| `PUT /api/routers/{id}` | `src/api/devices.rs` / `update` | D: validation, flags, secret preservation/clearing; O |
| `DELETE /api/routers/{id}` | `src/api/devices.rs` / `remove` | D: deletion and 404; O: ownership protection |
| `GET /api/routers/{id}/test-connection` | `src/api/devices.rs` / `test_connection` | O: authorization; local protocol fixtures in `routeros.rs` |
| `GET /api/backups` | `src/api/backups.rs` / `list` | F: type, device, day, search, sorting; O |
| `GET /api/backups/router/{id}` | `src/api/backups.rs` / `for_router` | O: device authorization |
| `POST /api/backups/routers/export-all` | `src/api/backups.rs` / `export_all` | D: disabled devices skipped; `backup::run_all` returns per-device results |
| `POST /api/backups/routers/binary-all` | `src/api/backups.rs` / `binary_all` | D: disabled devices skipped; `backup::run_all` returns per-device results |
| `POST /api/backups/router/{id}/export` | `src/api/backups.rs` / `export` | D: policies and unsupported SwOS export; O: ownership; SSH adapter results in validation report |
| `POST /api/backups/router/{id}/binary` | `src/api/backups.rs` / `binary` | D: policies; O: ownership; SwOS fixture and SSH adapter checks |
| `POST /api/backups/router/{id}/upload/{backup_id}` | `src/api/backups.rs` / `upload` | O: both resources owned; SSH/SFTP adapter check; upload does not restore |
| `DELETE /api/backups/{id}` | `src/api/backups.rs` / `remove` | F: file and metadata deletion; O: ownership |
| `GET /api/backups/{id}/download` | `src/api/backups.rs` / `download` | F: exact binary bytes and Content-Type; O |
| `GET /api/backups/{id}/view` | `src/api/backups.rs` / `view` | F: export text and rejection of binary content; O |
| `POST /api/backups/{id}/email` | `src/api/backups.rs` / `email` | O: ownership; `smtp_delivery_with_attachment_against_local_fixture` for SMTP |
| `GET /api/backups/{left}/diff/{right}` | `src/api/backups.rs` / `diff_json` | F: statistics/text and rejection of binary content; O: both resources |
| `GET /api/backups/{left}/diff/{right}/html` | `src/api/backups.rs` / `diff_html` | F: HTML; O; `escapes_configuration_and_file_names` |
| `POST /api/backups/bulk` | `src/api/backups.rs` / `bulk` | F: ZIP, no partial mutation for an invalid ID, deduplication; O |
| `GET /api/settings` | `src/api/system.rs` / `settings` | `settings_secrets_round_trip_without_public_exposure`; S: saved response |
| `GET /api/settings/scheduler-status` | `src/api/system.rs` / `scheduler_status` | S: timezone and five jobs; cron/DST tests in `scheduler.rs` |
| `PUT /api/settings` | `src/api/system.rs` / `update_settings` | S: cron, preserve/clear secrets, invalid input rejected before save |
| `POST /api/settings/reveal-ssh-key` | `src/api/system.rs` / `reveal_ssh_key` | S: incorrect and correct account password |
| `POST /api/settings/test-email` | `src/api/system.rs` / `test_email` | S: configuration errors; local SMTP provider fixture |
| `POST /api/settings/test-pushover` | `src/api/system.rs` / `test_pushover` | S and `missing_pushover_settings_fail_before_network`; no live account delivery |
| `GET /api/logs` | `src/api/system.rs` / `logs` | L: limit; original id/message/timestamp model |
| `DELETE /api/logs/older-than/{days}` | `src/api/system.rs` / `delete_logs` | L: validation and actual removal of old records |
| `GET /api/health` | `src/api/system.rs` / `health` | L: public health check, version, configurable prefix |
Backup list parameters are `search`, `backup_type=export|binary`, `router_id`,
`created_on=YYYY-MM-DD`, `sort_by=created_at|file_name|backup_type|router_name`, and
`order=asc|desc`. Logs retain `limit` in the range 1–500. Bulk operations accept
`action=delete|download` and `backup_ids`. Login accepts `username`/`password`;
password change uses `current_password`/`new_password`; SSH key reveal uses
`password`; preferences use `preferred_language`/`preferred_font`. A login response
contains `access_token`, `token_type=bearer`, and `user`.
## Models and source fields
Models are explicitly typed in `src/models.rs`. `src/db.rs` implements persistence,
current-schema validation, and explicit import. `src/schema.sql` is the complete
fresh-install schema. Revision 4 does not run incremental schema migrations at startup.
| Model / group | Preserved or added fields | Notes |
| --- | --- | --- |
| User | `id`, `username`, `password_hash`, `created_at`, `preferred_language`, `preferred_font` | Password hashes are never serialized. The user DTO has the same four public fields as the source. |
| Router identity | `id`, `owner_id`, `name`, `device_type`, `host`, `port`, `ssh_user`, `created_at` | Every device/backup access checks ownership. |
| Router credentials | `ssh_key`, `ssh_password` | Public responses contain null and presence flags; values remain stored in the database. |
| Router SwitchOS source | `use_global_switchos_credentials` | Nullable addition: true selects the global pair, false selects the local pair, null preserves legacy fallback. Changing mode preserves local secrets. |
| Router connection result | `last_connection_status`, `last_connection_tested_at`, `last_connection_error`, `last_connection_hostname`, `last_connection_model`, `last_connection_version`, `last_connection_uptime`, `last_connection_transport`, `last_connection_server`, `last_connection_auth_mode`, `last_connection_http_status`, `last_connection_backup_available` | Unknown values remain null; errors are not presented as success. |
| Router policies | `disable_all_backups`, `disable_export_backups`, `disable_binary_backups`, `disable_ping` | Enforced server-side, including scheduled and batch operations. |
| Router DTO | `effective_username`, `uses_global_ssh_key`, `has_effective_ssh_key`, `uses_global_switchos_credentials`, `has_effective_password`, `has_own_password`, `supports_export`, `supports_restore_upload` | Derived from the selected credential source and device type without disclosing secrets. |
| RouterTestConnection | `success`, `tested_at`, `model`, `uptime`, `hostname`, `version`, `error`, `transport`, `server`, `auth_mode`, `http_status`, `backup_available` | The same result is persisted in the device's last_connection fields. |
| RouterPingStatus | `router_id`, `reachable`, `latency_ms`, `disabled`; optional `error` | Batch response contains `items`. Diagnostic errors distinguish missing ICMP permissions from an ordinary timeout. |
| Backup | `id`, `router_id`, `file_path`, `file_name`, `backup_type`, `checksum`, `created_at` | Additional DTO fields: `router_name`, `device_type`, `file_size`. Import copies existing bytes without converting their format. |
| OperationLog | `id`, `message`, `timestamp` | No invented level/component/router_id fields. |
| Dashboard | `routers_count`, `export_count`, `binary_count`, `total_backups`, `storage`, `recent_logs` | Device/backup counts are owner-scoped; logs remain global as in the source. |
| StorageStats | `total`, `used`, `free`, `folder_used`, `usage_percent` | `usage_percent` measures the data directory against total disk capacity, matching the source. |
| Settings retention | `backup_retention_days`, `log_retention_days` | 0 means an age threshold of zero days, matching the original retention function; it does not disable cleanup. |
| Settings scheduling | `export_cron`, `binary_cron`, `retention_cron`, `enable_auto_export`, `connection_test_interval_minutes` | Five-field cron, configured timezone, Monday=0 as in APScheduler. |
| Settings device defaults | `global_ssh_key`, `default_switchos_username`, `default_switchos_password` | Revealing the global key requires password verification. |
| Settings notifications | `pushover_token`, `pushover_userkey`, `notify_failures_only`, `smtp_host`, `smtp_port`, `smtp_login`, `smtp_password`, `smtp_notifications_enabled`, `recipient_email` | SMTP, attachments, Pushover, provider tests, and failures-only mode. |
| Settings DTO | `id`, `has_global_ssh_key`, `has_default_switchos_credentials` | Additional `has_smtp_password`, `has_pushover_token`, `has_pushover_userkey`, and `has_default_switchos_password` flags support editing masked secrets. |
| Diff JSON | `left_backup_id`, `right_backup_id`, `left_file_name`, `right_file_name`, `diff_text`, `stats`, `lines` | Statistics: added/removed/modified/context. Lines: type, left_number/right_number, left_text/right_text. Standalone `diff_html` is generated only by the dedicated HTML endpoint to avoid duplicating large payloads in the browser response. |
| SchedulerStatus | `timezone`, `running`, `jobs` | Job fields: key, label, enabled, cron, description, description_params, valid, next_runs, error. |
## Client views and behavior
| Source view / behavior | Implementation |
| --- | --- |
| `/login`, `/register`, session restoration, auth/guest guards, Bearer token | `web/js/core.js`, `web/js/views.js`; actual auth API |
| `/` dashboard | API counts, storage, recent backups/logs, and batch actions |
| `/devices`, alias `/routers` | List, search, device state, create, and edit |
| `/devices/:id`, alias `/routers/:id` | Connection test, metadata, and backups; no RSC exports or diff for SwitchOS; comparison only for RouterOS text exports |
| `/files` | Filters, sorting, selection, ZIP, deletion, preview, email, and diff |
| `/diff-configs` | Two exports, counts, line numbers, side-by-side/unified views, and search |
| `/logs` | Original messages and timestamps, cleanup by age in days |
| `/settings` | Retention, cron, global credentials, SMTP/Pushover, tests, and next runs |
| `/settings?tab=automation`, alias `/schedule` | Flat schedule controls; no separate Schedule navigation item |
| `/settings?tab=account`, alias `/change-password` | Password change in account settings; old route redirects to that tab |
| `/docs`, `/openapi.json`, alias `/redoc` | Local Swagger UI, full contract, authentication, and real API requests |
| Language and font | `pl`, `en`, `es`, `no`; `default`, `adwaita_mono`, `hack`; persisted user preferences |
| Theme, sidebar, responsive layout, toasts, confirmations | `web/css/app.css`, `web/js/core.js`, `web/js/views.js`; no sample devices inserted into the database |
| CSS/JavaScript delivery | Modular `web/` sources bundled into one `app.css` and one `app.js`, embedded in the executable |
Default font families preserve the source stacks: Inter/system UI for content and
Roboto Mono/IBM Plex Mono/SFMono-Regular/Consolas for headings. Adwaita Mono and Hack
preferences select their respective stacks. Specific fonts depend on what is
installed on the client device.
## Services, configuration, and data
| Area | Implementation | Tests / evidence |
| --- | --- | --- |
| Argon2id and Python password compatibility | `security.rs` | `new_passwords_use_argon2id`, `verifies_python_passlib_fixture_without_reset`, `verifies_legacy_werkzeug_fixture`, `malformed_hashes_fail_closed` |
| JWT HS256, username subject, expiration, and nbf | `security.rs` | `jwt_has_original_username_subject`, `expired_and_wrong_algorithm_tokens_are_rejected`, `signed_malformed_or_missing_expiration_is_rejected`, `not_before_is_optional_but_must_be_valid_and_reached`, `verifies_independent_python_hmac_jwt_fixture` |
| Environment, SQLite URL, timezone, initial secret | `config.rs` | `generated_secret_survives_restart`, `accepts_legacy_sqlite_url_and_custom_prefix`, `rejects_unsafe_configuration` |
| Fresh schema and existing-schema validation | `db.rs`, `src/schema.sql` | Fresh initialization and incompatible-schema rejection regressions in `db.rs`; `generated_bootstrap_password_is_saved_and_valid` |
| Explicit database/file import | `db.rs` | `import_copies_files_and_preserves_source`, `failed_import_does_not_publish_partial_database`, `orphaned_legacy_backups_are_not_silently_skipped`, `import_rejects_corrupted_backup_before_publication`, `import_rejects_backup_symlink_outside_selected_root` |
| Import exclusivity and atomic publication | `db.rs` | `import_obeys_storage_lock_and_keeps_source_untouched`, `atomic_copy_publication_refuses_existing_destination` |
| Import wrapper | `import-python.sh`, `tests/import_launcher.rs` | `help_and_invalid_sources_do_not_build_or_import`, `resolves_source_and_preserves_arguments_and_destination_settings`, `forwards_import_failure_without_falling_back_to_serve` |
| Secret redaction and preservation | `models.rs`, `db.rs` | `settings_keep_secrets_and_clear_explicitly`, `invalid_patch_is_atomic`, `generic_serialization_does_not_leak_secrets`, `settings_secrets_round_trip_without_public_exposure` |
| RouterOS SSH export/binary, SFTP/SCP, host keys, timeout | `routeros.rs`, `backup.rs` | Controlled SSH/SFTP server checks in the validation report; no physical MikroTik supplied |
| Native SSH library fixes | `vendor/libssh2-sys` | Patch reproduction and `tests/run-security-regressions.sh`; sources, hashes, and limits in [dependency-security.md](dependency-security.md) |
| SwOS through the existing device model | `routeros.rs` | `switchos_digest_probe_and_binary_transfer_against_local_fixture`, `switchos_basic_fallback_and_html_rejection` |
| ICMP ping | `routeros.rs` | `native_icmp_loopback_and_disabled_flag`, `icmp_checksum_verifies_packet`; non-root Linux requires configured `net.ipv4.ping_group_range` |
| Atomic files, checksums, directory boundaries, device locks | `backup.rs` | `checksum_matches_sha256`, `file_names_cannot_escape`, `deletion_allows_missing_file_but_rejects_escape`, `symlink_cannot_escape_data_directory`, `device_lock_releases_on_drop` |
| Python/Rust timestamp retention | `backup.rs` | `supports_python_and_rust_timestamps`, `retention_uses_age_and_zero_is_not_disabled` |
| Diff and escaped HTML | `diff.rs` | `reports_all_line_types`, `escapes_configuration_and_file_names`, `final_newline_and_crlf_are_normalized`, `tracks_blank_lines_in_replacements`, `unified_output_matches_python_difflib_golden_fixtures` against 32 Python difflib fixtures |
| Cron and DST | `scheduler.rs` | `validates_cron_and_ranges`, `monday_zero_matches_apscheduler`, `timezone_and_dst_preview`, `named_weekdays_and_last_day`, `rare_calendar_schedule_is_not_lost` |
| SMTP/Pushover/error redaction | `notifications.rs` | `smtp_delivery_with_attachment_against_local_fixture`, `missing_pushover_settings_fail_before_network`, `error_redaction_covers_global_secret_fields` |
All source environment variables are mapped: `APP_NAME`, `APP_ENV`, `SECRET_KEY`,
`JWT_ALGORITHM` (HS256 only), `ACCESS_TOKEN_EXPIRE_MINUTES`, `DATABASE_URL`, `DATA_DIR`,
`ALLOW_REGISTRATION`, `API_PREFIX`, `TIMEZONE`, `DEFAULT_ADMIN_USERNAME`,
`DEFAULT_ADMIN_PASSWORD`, `SMTP_STARTTLS`, `SMTP_TIMEOUT_SECONDS`, and `CORS_ORIGINS`.
`APP_PORT` controls the default port. `LISTEN_ADDR`, `DATABASE_PATH`,
`SSH_TIMEOUT_SECONDS`, `SSH_HOST_KEY_POLICY`, and `MAX_PARALLEL_BACKUPS` are explicit
deployment extensions.
## Intentional security and reliability changes
- No shared `admin/admin` password. A fresh installation generates an initial
password in a private file unless one is configured. Registration is disabled
by default and can be explicitly enabled with `ALLOW_REGISTRATION=true`.
- Public responses redact secrets. Blank/null secret input preserves the value;
explicit `clear_*` removes it. Upload and diff check ownership of both resources.
- SSH host keys are verified. Default `accept-new` permits the first connection,
records the key, and rejects changes. Explicit `strict` requires a trusted
known_hosts entry. A host key is distinct from a user's authentication private key.
- Directory and symlink checks protect backups. ZIP responses stream a temporary
file rather than buffering the complete archive in memory. Bulk downloads are
limited to 2 GiB, and email attachments to 25 MiB; exceeding a limit is an error.
- New backup filenames include a finer-grained timestamp and random suffix so
concurrent operations cannot overwrite each other. Imported names and contents
remain supported.
- External service failures return controlled HTTP 502 responses with secret values
redacted. A notification failure does not remove or invalidate a successful backup.
- Naive SQLite timestamps are presented as UTC ISO 8601, preserving the source's
actual time without relying on the browser timezone to interpret it.
- Structured diff corrects old blank-line numbering in replacement blocks: an empty
existing line is not treated as a missing line. Unified output is checked against
32 file-based fixtures generated by Python difflib.
- Empty cron expressions disable their schedules. Invalid expressions, invalid
ports, negative retention, and excessive request sizes are rejected. Jobs are
prevented from overlapping themselves.
- Revision 4 initializes only a fresh schema and validates an existing database
before use. There are no startup migrations or backup-layout conversions. Python
import is explicit, reads the source without modifying it, and requires a new
destination. See [data-import.md](data-import.md).
- Runtime is one Rust process with SQLite, scheduling, and an embedded interface.
Python, Node, Angular, and nginx are not required application processes.
## Concept image and inactive source code
| Item | Decision |
| --- | --- |
| Git integration / commits / synchronization | No source model, endpoint, or service exists; the concept screen is not represented as a working feature. |
| Arbitrary job CRUD | The source has system jobs controlled through settings; that behavior is preserved. |
| `/swos-beta/probe`, `/swos-beta/download` | The router was not included in `backend/app/api/__init__.py`; no new public beta API is activated. |
| Existing `switchos` type | Preserved: ordinary connection tests and binary backups invoke SwOS code through the original RouterService despite the inactive separate beta router. |
| Concept's restore action | The source only transfers a binary file to RouterOS. Upload is preserved without automatic restore or restart. |
| Invented network traffic or log levels | Views use actual API data rather than demo measurements or fields absent from the model. |
## Evidence limits
No production database, physical routers, device host keys, or provider accounts were
supplied. Local fixture checks do not establish compatibility with every firmware
version. For a deployment transition, follow the explicit
[import procedure](data-import.md), verify a connection to one real device, and
confirm that its backup can be downloaded. Starting the new server activates
schedules from the imported settings, so stop the old processes first. Release tests
do not automatically restore any device configuration.
+26
View File
@@ -0,0 +1,26 @@
# Security and dependency notes
Version 0.0.1 is distributed as source and uses the dependency versions pinned in `Cargo.lock`.
Key controls include:
- Argon2id for new passwords while retaining verification of imported PBKDF2 hashes;
- HS256 JWT validation with bounded expiry;
- request body size/deadline limits and login throttling;
- escaped UI output and restrictive response security headers;
- redaction of configured secrets from device-operation errors;
- SSH host-key verification with `accept-new` or `strict` policy;
- private data/storage permissions and a single-writer storage lock;
- non-root Alpine container runtime with dropped Linux capabilities and a read-only root filesystem;
- local Swagger UI without CDN dependencies.
The project patches `libssh2-sys` locally for audited upstream fixes. Provenance is documented in `vendor/libssh2-sys/PROVENANCE.md`.
The retained advisory scan artifacts are:
```text
docs/dependency-audit.json
docs/dependency-audit-metadata.json
```
Third-party license text is in `docs/THIRD_PARTY_NOTICES.txt`.
+46
View File
@@ -0,0 +1,46 @@
# SwOS backup support
SwOS/SwOS Lite devices are handled through their HTTP/HTTPS web interface rather than RouterOS SSH.
## Backup flow
The adapter:
1. normalizes the configured device URL;
2. probes the web interface;
3. obtains the authentication challenge when required;
4. attempts HTTP Digest authentication, then Basic authentication as fallback;
5. follows bounded same-device redirects and re-authenticates for the final URI when needed;
6. downloads `GET /backup.swb`;
7. rejects empty responses and obvious HTML/login/error pages before saving the `.swb` file.
Digest challenge responses are fully consumed and challenge cookies are preserved before retrying. `/backup.swb` starts with a fresh authentication state instead of reusing an unrelated homepage nonce.
## Connection test
The connection test reports web reachability separately from backup availability. A device can therefore be online while `/backup.swb` is unavailable or rejected.
## Credentials
A SwOS device may use either:
- global SwOS credentials from Settings; or
- credentials saved specifically for that device.
Legacy imported records retain the previous fallback behavior until an explicit source is selected.
## Troubleshooting HTML instead of `.swb`
If the application reports that SwOS returned HTML instead of `/backup.swb`:
- verify the configured scheme/host/port;
- verify the selected credential source and credentials;
- confirm that **Save Backup** works in the device web interface;
- confirm `/backup.swb` works with the same account/firmware;
- inspect the returned status/content type/title included in the application diagnostic.
Do not treat an HTTP `200` login page as a valid backup.
## Scope
Version 0.0.1 downloads SwOS backups. It does not restore/upload a SwOS configuration or reboot a switch.
+141
View File
@@ -0,0 +1,141 @@
# Interface — 0.0.1
The interface uses the application's real API. It does not populate sample devices,
simulate operation results, or present disconnected actions as working features.
## Build and runtime
`build.rs` reads `web/assets.list` and combines the sources into **one `app.js` and
one `app.css`**. Both files and `index.html` are embedded in the Rust executable.
Building or running the application does not require Node, Angular, a frontend
server, nginx, an external CDN, or a font service.
| Source file | Responsibility |
| --- | --- |
| `web/js/i18n.js` | Polish, English, Spanish, and Norwegian dictionaries, including completed missing source translations |
| `web/js/core.js` | API access, formatting, HTML escaping, forms, and dialogs |
| `web/js/views.js` | Views and data presentation |
| `web/js/app.js` | Routing, sessions, and user actions |
| `web/css/app.css` | Responsive layout, themes, components, and diff styling |
Revision 4 translates project documentation into English. It does not change the
interface's four supported languages or users' saved language preferences.
## Views and actions
| Route | Features |
| --- | --- |
| `/login`, `/register` | Sign-in, registration only when enabled on the server, and validation errors |
| `/` | Counts, recent backups, saved connection status, disk usage, seven-day activity, logs, and batch actions with partial results |
| `/devices` | Device search/filtering, creation, editing, ping, and connection tests |
| `/devices/:id` | Metadata, the latest test, backups, policies, and device deletion; RSC exports only for RouterOS |
| `/files` | Filtering, sorting, download, RSC preview, email, binary upload, ZIP, and bulk deletion |
| `/diff-configs` | Export selection, side-by-side and unified views, line numbers, statistics, search, changes-only mode, synchronized scrolling, and diff/HTML download |
| `/logs` | Actual messages and timestamps, search, pagination, and confirmed cleanup |
| `/settings` | Retention, automation, credentials, SMTP, Pushover, and account preferences |
| `/settings?tab=automation` | Flat scheduled-task controls, cron expressions, and actual next execution times |
| `/settings?tab=account` | Account tab with password change, matching-password confirmation, and language/font preferences |
| `/schedule` | Compatibility redirect to `/settings?tab=automation` |
| `/change-password` | Compatibility redirect to `/settings?tab=account` |
`/routers` and `/routers/:id` are device-route aliases. A Git screen and a standalone
SwOS beta module were not part of the active source API, so they are not presented
as available features. Binary upload writes a file to the device; it does not
perform a restore or restart automatically.
## Behavior details
- Operations show results after receiving an API response. Deletion and upload
require confirmation. Batch backup results include failures and skipped devices.
- Comparison is available only for RSC exports. The bulk compare action appears
when exactly two exports are selected. Binary and SwitchOS files have no diff
action. SwitchOS has no exports tab, export action, or export policy control.
Diff errors use the existing toast notification without an extra error block
inside the form.
- Secret fields are never prefilled. A blank field preserves the secret; a separate
explicit control clears it. Revealing the global SSH key requires the account
password. Closing the dialog removes the revealed text from the interface.
- The SwitchOS form has an explicit credential-source selector.
`use_global_switchos_credentials=true` uses only the current global credentials;
`false` uses only the device's credentials. In global mode, the UI sends neither
`ssh_user` nor `ssh_password` nor their clear controls. Stored local values are
retained. `has_own_password` supplies a presence hint for the empty password field,
never the password itself. A legacy record with `null` preserves its original
fallback behavior on an ordinary save until the user explicitly selects a source.
- The Bearer token is kept in the current tab's `sessionStorage`. An existing
`routeros_token` from the old UI is moved into the session only after successful
verification through `/auth/me`. Signing out clears the client session.
- `API_PREFIX` comes from page metadata inserted by the server.
- The server supplies `ALLOW_REGISTRATION` through
`<meta name="allow-registration" content="false">`. If the tag is absent or its
value is anything other than `true`, the registration link is hidden and
`/register` redirects to sign-in. The API independently enforces the same setting.
- Password changes live in **Settings → Account**, without a separate navigation
item. The previous direct route continues to work through a redirect.
- Language and font are saved through `/auth/preferences`. Theme and sidebar width
are browser preferences. Font stacks preserve Inter/system UI, Roboto Mono/IBM
Plex Mono, Adwaita Mono, and Hack. If a font is not installed, the browser uses
the configured fallback; no font is downloaded at runtime.
- Names, logs, exports, and diff content are escaped as text before presentation.
Server-generated diff HTML is downloaded as a file, not inserted into the app DOM.
- Weekly scheduling writes weekday names `mon`–`sun`. Imported numbers preserve
APScheduler's actual semantics: **0 = Monday**, **6 = Sunday**. The old interface
described these numbers inconsistently; this editor shows the actual scheduled day.
- Automation is part of Settings without a separate navigation item.
`/settings?tab=schedule` also redirects to `automation`. Schedule fields and next-run
information use plain rows rather than nested cards or bordered panels.
- A small **API documentation** footer link opens local `/docs`. The backend serves
this page separately from the SPA router.
- If the host disallows ICMP socket creation, ping shows an unknown state with the
diagnostic reason. Missing process permissions are not reported as a router outage.
## Interface tests
These are development tools. Node and Playwright are not application dependencies.
```sh
node web/tests/core.test.cjs
```
The core suite runs without npm packages. It covers translations, HTML injection
protection, secret handling in forms, UTC dates, cron semantics, and presentation of
actual data structures. The browser suite requires Playwright and Chromium. It
starts an isolated server with controlled API responses and exercises sign-in,
device CRUD, backups, diff, settings, languages, policies, confirmation dialogs, and
mobile layout. It does not connect to routers or notification services.
For an optional, repeatable browser-tool installation outside the project, run the
following from the project root in one shell. Node.js and npm must be available:
```sh
MBS_UI_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mbs-ui-tests.XXXXXX")"
npm install --prefix "$MBS_UI_TEST_DIR" --no-audit --no-fund --save-exact playwright@1.62.1
export NODE_PATH="$MBS_UI_TEST_DIR/node_modules${NODE_PATH:+:$NODE_PATH}"
export PLAYWRIGHT_BROWSERS_PATH="$MBS_UI_TEST_DIR/browsers"
node "$MBS_UI_TEST_DIR/node_modules/playwright/cli.js" install chromium
node web/tests/smoke.cjs
```
The npm package and downloaded browser remain under the temporary directory named
by `MBS_UI_TEST_DIR`; the project does not gain `node_modules`, `package.json`, or
an npm lockfile. Remove that temporary directory when finished. If Chromium is
already installed, skip its download and set `MBS_CHROMIUM` to the existing
executable path. These environment variables also apply to the live-server check
below.
To check an actual running server without changing its data:
```sh
MBS_BASE_URL=http://127.0.0.1:5581 \
MBS_TEST_USERNAME=admin \
MBS_TEST_PASSWORD='test-account-password' \
node web/tests/live.cjs
```
`MBS_CHROMIUM` optionally selects an existing Chromium executable.
`MBS_SCREENSHOT_DIR` saves local screenshots for inspection. The live check verifies
views against the actual backend, JavaScript/API/CSP errors, and responsive layout;
it does not create backups, upload files, or send notifications. Revision-specific
execution results are recorded in [the validation report](validation.md); the
presence of a test in the source tree does not imply that it has been rerun for
every revision.
+329
View File
@@ -0,0 +1,329 @@
# Validation report — revision 5 working copy
Date: **28 October 2026**. This section applies to the revision 5 changes only; the
older revision 4 and revision 3 results below remain historical evidence for their
respective source revisions.
| Area | Current result | Scope |
| --- | --- | --- |
| JavaScript syntax | **PASS** | `node --check web/js/*.js` |
| JavaScript unit tests | **16/16 PASS** | `node web/tests/core.test.cjs` |
| Rust compile/tests | **NOT RUN in this environment** | `cargo` / `rustc` are not installed in the available runtime; no current Rust pass is claimed |
| Browser smoke/live tests | **NOT RUN in this environment** | Playwright is not installed; the test files could not start |
| SwitchOS protocol comparison | **REVIEWED** | Compared directly with the supplied Python `SwosBetaService`; local `requests.HTTPDigestAuth` output was also checked for header formatting |
| Physical SwitchOS device | **NOT TESTED** | Requires the user's device/firmware for final acceptance |
The revision 5 patch changes SwitchOS authentication/probing, settings layout, and
RSC diff payload/rendering. Digest handling now accepts a changed nonce/challenge for
`/backup.swb` after homepage authentication and re-signs redirects for their final URI.
A physical-device check remains required to confirm the original 502 scenario is
resolved on the affected firmware.
---
# Validation report — 0.0.1, revision 4
Date: **27 October 2026**. This report separates completed revision 4 verification
from historical results. Historical checks are explicitly labeled and are not
presented as newly executed revision 4 checks.
The distribution contains **source only**: Rust code, `Cargo.lock`, interface sources,
tests, and the patched library under `vendor/`. It does not ship an executable or
`bin/`, `target/`, or `storage/` directories. The input archives contained no production
database or device credentials.
## Current revision 4 results
Revision 4 replaces automatic schema/data/layout conversion with fresh initialization
and an explicit Python importer. It also embeds API documentation directly from
checked-in input files and translates project documentation into English.
| Area | Result | Evidence and scope |
| --- | --- | --- |
| Rust dev/test build and suite | **111/111 PASS** | 95 unit tests, 11 API contract tests, 3 import-launcher tests, and 2 OpenAPI tests; no failures or skipped tests |
| Fresh installation and incompatible database | **PASS** | Fresh schema creation and restart; old/foreign/empty existing databases rejected without conversion or backup relocation |
| Explicit Python importer | **PASS** | Unit regressions plus six actual CLI/HTTP acceptance scenarios; source selection, unchanged originals, preserved records/files, and refusal to overwrite a destination |
| Import launcher | **3/3 PASS** | Validation before build, argument/path preservation including spaces and CDPATH, and import failure propagation |
| Source launchers and ignore rules | **16/16 PASS** | Isolated Cargo/Docker stubs, build-before-start, errors, arguments, working directories, target-directory overrides, and private/generated file exclusions |
| API documentation embedding | **PASS** | `/docs`, `/openapi.json`, local versioned assets, matching content hashes, custom prefix, and English documentation |
| Release-profile check | **PASS** | `cargo check --release --locked --offline` after removing all six previous generated documentation files; no `OUT_DIR/docs.*` files recreated |
| Formatting and static analysis | **PASS** | Package-scoped formatting check and `cargo clippy --all-targets --locked --offline -- -D warnings` |
| JavaScript unit tests | **16/16 PASS** | Existing interface behavior, translations, forms, and credential-source controls |
| Chromium with actual Rust and a SwOS fixture | **8/8 PASS** | Global credentials, backup bytes/layout, HTML rejection, English Swagger rendering, public requests, and OAuth-authorized requests |
| Chromium with actual Rust navigation | **PASS** | Eight routes, settings redirects, responsive layout, CSP, no API/JavaScript errors, and sign-out |
| Dependency integrity | **PASS** | All 91 libssh2 checksum entries and 5 Swagger UI assets verified; Cargo.lock unchanged from the documented historical audit |
| Release executable build | Not performed for revision 4 | The deployment launcher builds release source on the target host |
| Docker / systemd execution | Not performed for revision 4 | No container or service-manager execution result claimed |
The actual import acceptance run used a generated database matching the supplied
Python SQLAlchemy models and the original Compose directory layout. Its Passlib
PBKDF2-SHA256 password hash was generated independently with Python's standard
library. The run verified:
1. Startup rejects that original database without changing any source file or
moving a backup.
2. Explicit import discovers the Compose database and creates a separate target;
it neither executes nor copies the source `.env`.
3. IDs, ownership, password hashes, database secrets, logs, filenames, checksums,
and every backup's bytes are preserved in the current schema and layout.
4. A second import refuses the existing target without overwriting it.
5. The imported account can sign in and download all three fixture backups after
the first start and a subsequent restart.
6. Ordinary startup in another fresh directory creates an empty installation,
without importing the nearby Python data.
Unit regressions additionally retain committed WAL data with an existing or
missing source SHM file, compare source sidecar bytes before/after import, reject
ambiguous or overlapping paths, and verify validation precedes secret/lock writes.
No production database was used. Filesystem interruption and synchronization
failures remain subject to the explicit cleanup limits in [data-import.md](data-import.md).
The browser run executed public `/health` and an authenticated device-list request
through the actual English Swagger UI. All documentation assets stayed on the
same origin, with no JavaScript or CSP errors. The build/profile check and tests
ran with the old generated documentation files removed; the updated documentation
handler embeds checked-in input files directly.
Evidence is recorded in `revision4-release-check.log`, `revision4-rust-tests.log`,
`revision4-clippy.log`, `revision4-format.log`, `revision4-ui-core.log`,
`revision4-live-import.json`, `revision4-live-switchos.json`, `revision4-ui-live.log`,
`revision4-source-start-checks.json`, and `revision4-vendor-verification.json`.
These are acceptance-environment reports, not files promised in the source archive.
The project contains the Rust, launcher, and interface regression tests.
The installation sequence is documented in [data-import.md](data-import.md).
There is no automatic upgrade, startup conversion, or migration recovery framework.
## Historical revision 3 results
Revision 3 used actual dev/test builds with Rust 1.99.1, edition 2024. It did not run
a revision 3 release build, Docker container, or physical MikroTik acceptance test.
| Area | Historical revision 3 result | Scope and method |
| --- | --- | --- |
| Dev/test compilation | PASS | Actual application and test compilation with Rust 1.99.1, edition 2024 |
| Rust unit tests | **91/91 PASS** | Data, then-current migrations, security, files, diff, cron, adapters, and notifications |
| HTTP contract tests | **11/11 PASS** | Actual Axum router and SQLite, authorization, file operations, and SwitchOS credential-source modes |
| OpenAPI/documentation tests | **2/2 PASS** | All 36 operations, route/custom-prefix compatibility, and local Swagger assets without a CDN |
| Total Rust tests | **104/104 PASS** | 91 unit + 11 API + 2 OpenAPI; zero failures or skipped tests |
| JavaScript tests | **16/16 PASS** | UI logic, forms, schedules, translations, and interface regressions |
| Chromium workflow with controlled API | PASS | **113 requests** to a local fixture API, full interface workflow, no JavaScript errors |
| Final formatting | **PASS** | `cargo fmt --package mikrotik-backup -- --check` |
| Final Rust static analysis | **PASS** | `cargo clippy --all-targets --locked -- -D warnings` |
| Chromium + actual Rust, Swagger UI, and SwOS | **8/8 PASS** | Browser, running backend, SQLite, and an independent local HTTP Digest SwOS fixture |
| Chromium + actual Rust navigation | **PASS** | Eight routes, old-route redirects, responsiveness, CSP, no API/JS errors, and sign-out |
| Vendored integrity recheck | **PASS** | All **91 entries** in `vendor/libssh2-sys/SHA256SUMS` and **5 Swagger UI files** matched; `Cargo.lock` unchanged |
| Revision 3 release build | Not performed | The deployment launcher builds release on the target host |
| Revision 3 Docker execution | Not performed | No image/container execution result established |
These results were recorded in `revision3-rust-tests.log`, `revision3-ui-core.log`,
`revision3-ui-workflow.log`, `revision3-ui-live.log`, `revision3-live-switchos.json`,
`revision3-clippy.log`, `revision3-format.log`, and
`revision3-vendor-verification.json` in the acceptance tooling directory. These are
verification-environment logs, not files promised as part of the source archive.
The Rust and JavaScript test sources are included in the project.
### Historical migration, file, and concurrency checks
**The automatic migration behavior in this subsection belonged to revision 3 and
has been removed in revision 4.** It is retained here only to identify the scope of
the historical 91-unit-test result.
Those tests preserved IDs, ownership, password hashes, settings, and secrets while
upgrading old schemas. Migration 002 kept
`use_global_switchos_credentials = null` for existing devices. Migration 003 added
a persistent backup-layout journal. Fresh backups, imports, and startup conversions
used `DATA_DIR/backups/binary/<router_id>/` and
`DATA_DIR/backups/exports/<router_id>/`; renaming a device did not change the directory
ID, and the original `file_name` was preserved.
Checks covered a database snapshot before path changes, file copying before metadata
transactions, SHA-256, and idempotent restart. Fault cases interrupted the logical
process after plan persistence, after verified copying, and between file publication
and journal acknowledgement. Restart did not overwrite unrelated files, even with
identical bytes. Missing/modified sources, corrupted copies, tampered journal paths,
and snapshot symlinks prevented partial activation of changed records.
Separate cases covered source-preserving import, target conflicts, storage/database
locks, symlinks outside the selected source, and a dangling destination database
symlink. **Sixteen threads** concurrently created initial device/type directories;
a directory encountered after a race was checked again for symlinks.
Retention and deletion operated on active files and preserved originals retained by
that revision, even if the newer active copy was absent. Tests forced both a SQLite
DELETE rejection and a failed compensating rename; the remaining copy had to survive
as a recovery file. Code review also examined directory synchronization after imported
database publication and backup preservation following a later failure. No final
fsync failure, physical power loss, or storage hardware failure was injected.
Revision 4 uses a new destination and an explicit import instead of this startup
conversion mechanism. Historical results do not validate the replacement until its
own checks have completed.
### Historical credentials, SwOS adapter, and HTTP contract checks
Revision 3 API tests covered all three SwitchOS credential modes: a complete global
pair, a complete local pair, and legacy field-by-field fallback. Cases included
creation, partial PUT, ordinary saves that preserve legacy semantics, global changes
without rewriting device records, and returning from global mode to local mode
without losing the stored password. They verified no implicit fallback in explicit
modes, a clear missing-username error, empty-password support, and masked responses.
Adapter tests used **local HTTP servers with controlled responses**. They covered
Digest, Basic, and authentication retries when a public HTML page did not return a
401 challenge. Checks included the correct system and backup endpoints, the Digest
request URI, a global credential pair, explicit URL ports, and actual response-byte
transfer and validation. HTML/login pages could neither be saved as backups nor
produce a positive availability result. A missing endpoint could not be replaced
with a public homepage; redirects did not forward credentials; the timeout covered
the whole operation. These were adapter/protocol tests, not hardware acceptance for
a particular SwOS model.
Other repeated cases covered authorization, ownership, JSON/form login, preferences,
password changes, and the bounded password-operation pool. Independent fixtures
verified Passlib/Werkzeug passwords and HS256 compatibility, invalid `exp`/`nbf`
rejection, secret preservation/clearing, download bytes, RSC previews, and ZIP.
Unified diff passed **32 Python difflib fixtures**. Local SMTP and Pushover tests did
not send to production recipients; the SMTP fixture did not establish STARTTLS
compatibility with an external provider.
The two documentation tests covered all **36 API operations**, actual route coverage,
a custom prefix, and documentation assets served without a CDN. A completed Chromium
run against an actual Rust process loaded all 36 operations, executed public
`/health`, signed in through **Authorize**, and fetched an authenticated device list.
There were no external-server requests, JavaScript errors, or CSP violations.
That same run created a SwitchOS device through the form using global credentials
without copying secrets into the device. The connection test and backup used Digest
with a `/sys.b` challenge and the correct `/backup.swb` URI. Downloaded bytes, SHA-256,
and `backups/binary/<router_id>/` were checked, including valid bytes mislabeled
`text/html`. Rotating the global username/password took effect without editing the
device. Actual HTML was rejected with HTTP 502, no backup record was added, and the
connection test did not report availability. The fixture device, backups, and global
test credentials were removed afterward.
### Historical revision 3 interface checks
The mocked-API Chromium workflow made **113 requests**. It covered login and
registration, device forms, secret preservation, the global → local → global → local
sequence, saving legacy `null` mode, downloads, binary upload, and bulk deletion.
It checked RSC-only diff actions, hidden SwitchOS export/unsupported actions,
toast errors, log filters, preferences, and password changes through the form.
Checks also covered flat daily/weekly/custom automation controls, old-route redirects,
responsive layout, themes, escaping, and sign-out. A separate actual-Rust run visited
`/devices`, `/files`, `/diff-configs`, `/logs`, `/settings`, `/schedule`,
`/change-password`, and `/`. It confirmed the automation and account redirects,
the flat automation layout, a mobile view without horizontal page scrolling, and
sign-out. Neither workflow had JavaScript errors; the live-server run also had no
API or CSP errors.
## Historical revision 2 checks
These separate runs are **not described as rerun in revision 3 or revision 4**.
The ordinary test suite reran its included SSH unit tests during revision 3; the five
scenarios below were a separate acceptance run with an actual application process.
| Area | Historical revision 2 result | Scope |
| --- | --- | --- |
| SSH trust over an actual local protocol | **5/5 PASS** | Default `accept-new`, repeated pin verification, changed-key rejection, invalid private key after host verification, and four concurrent first contacts |
| Launchers and project rules | **16/16 PASS** | Bash with isolated Cargo/Docker substitutes; build-before-start, errors, arguments, paths with spaces, `CARGO_TARGET_DIR`, Compose, `.env`; 31 ignored paths and 25 retained source paths |
| Chromium + actual Rust | PASS | Eight then-current routes, responsive layout, CSP, sign-out, password change through Settings, and signing in again |
| RustSec audit | PASS | **270 dependencies, 0 reported vulnerabilities, 0 warnings**, no ignored advisories |
| Vendor integrity | PASS | All **91 SHA256SUMS entries** matched |
Revision 3's `Cargo.lock` was unchanged from that audit. This does not mean the
advisory database was fetched again or a new scan was performed. The historical scan
and its metadata remain in [dependency-audit.json](dependency-audit.json) and
[dependency-audit-metadata.json](dependency-audit-metadata.json).
SSH cases used loopback, generated test keys, and real handshake/authentication.
Launcher checks used substitute CLIs; **they neither built release nor ran Docker**.
## Historical initial acceptance — before revision 2
Initial acceptance included a release build and actual local SSH/SFTP/SCP sessions
emulating RouterOS responses. It covered password, own-key and global-key
authentication, identity/resource, exports, binary backup, download, upload, SHA-256,
and remote temporary-file cleanup. Truncated SCP, negative ACK, and nonzero final
exit status were rejected. These runs are not new revision 3 or revision 4 tests.
A historical scheduler check removed a controlled expired file and its SQLite record.
Other checks covered restart, persistence, `healthcheck`, `check-config`, rejecting
a second instance, and SIGTERM during an incomplete request; SQLite reported
`integrity_check = ok`. A separate libssh2 C test exercised short/oversized AES-GCM
blocks and overflowing username lengths against earlier debug and release archives.
Patch reproduction confirmed all seven modified C files matched.
At that time, the launcher could run a bundled executable without Cargo. **That path
has been removed.** Current `start.sh` builds the source; the historical release
artifact is not included in this source archive. Backport details are in
[dependency-security.md](dependency-security.md).
## Limits and reproduction
- **Hardware and data:** no physical MikroTik or unavailable production database was
tested. Local fixtures do not establish compatibility with every model/firmware.
- **Network:** local SMTP/Pushover checks do not verify external provider accounts
or STARTTLS. The ICMP test permits an explicit socket-permission failure; PASS
alone does not prove an echo reply when the environment denied socket creation.
- **Deployment:** no revision 4 release executable was linked or run, and Docker
and systemd were not executed. The release-profile check, dev/test compilation,
and live debug-server acceptance are separate completed checks.
- **Dependency security:** the historical RustSec result applies to a specific
advisory set, date, and lockfile, not every possible vulnerability. System
OpenSSL, zlib, and glibc updates and new advisories remain relevant.
To reproduce local verification gates:
```bash
cargo fmt --package mikrotik-backup -- --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked
cargo check --release --locked
node web/tests/core.test.cjs
```
Run `cargo audit --file Cargo.lock --json` for a new dependency scan. Running
`./start.sh` builds release and starts the application on the deployment host.
`cargo check --release` checks the release profile but does not produce a tested
release executable. These commands are instructions, not additional recorded passes.
[ui.md](ui.md) describes browser checks. Fixture servers and acceptance tools are
not required for normal application operation.
## Revision 6 targeted SwitchOS change
The source adds a regression fixture for a SwitchOS server that sends a Digest
challenge together with `Set-Cookie` and returns an HTML login page if the cookie is
missing from the authenticated `/backup.swb` retry. The adapter now preserves that
cookie, drains the challenge response before retrying, forces HTTP/1.x, and starts the
backup request with fresh authentication state.
The current execution environment does not contain `cargo`, `rustc`, or `rustfmt`, so
the Rust test suite and compiler checks were **not executed for revision 6 here**. The
new test is included for execution on the build/deployment host with `cargo test --locked`.
## Revision 7 targeted UI, diff, container, and importer review
The browser diff now requests a compact JSON response that omits the duplicated unified
text and HTML documents. The UI renders at most 200 structured rows per page, filters
before rendering, and fetches the full unified `.diff` text only when the user explicitly
downloads it. Semantic RSC analysis is included alongside the line diff; it ignores
comments/export metadata and command ordering, groups changes by RouterOS section,
pairs recognizable commands, and flags selected security/network/service sections.
Current JavaScript verification in the patch environment:
- `node --test web/tests/core.test.cjs`: **16/16 PASS**.
- `node --check web/js/*.js`: **PASS**.
- Playwright browser acceptance: **NOT RUN** because the Playwright Node module is not installed.
- Rust compiler/tests: **NOT RUN** because `cargo` and `rustc` are unavailable.
- Docker build/runtime: **NOT RUN** because the Docker CLI/daemon is unavailable.
The supplied Python/TypeScript SQLAlchemy models were compared with `src/schema.sql`.
All legacy columns in `users`, `routers`, `backups`, `operation_logs`, and
`global_settings` are represented in the Rust schema; the Rust `routers` table adds
`use_global_switchos_credentials`. The importer copies supported source columns into
a new target, validates referenced backup files and recorded checksums, and publishes
the database last. No production database was supplied, so this is a source/schema
review plus existing importer regression coverage, not a production-data migration run.