first commit
This commit is contained in:
commit
32a7ba8e8f
168 files changed
+123856
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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' });
|
||||
}());
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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":{}}
|
||||
@@ -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.
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user