Martin's Blog

6. Rebuilding the Operator Console

Once the backend ran, I could query it through REST and MongoDB. I still could not see RCS as an operator saw it. The product was designed around a large Adobe AIR application whose console decided which sections a user could see, loaded and patched shared collections, translated a nested operation graph into screens, rendered about twenty evidence types, built configuration graphs, and turned asynchronous backend pushes into visible state.

Reconstructing that interface was therefore not cosmetic. The console was a substantial part of the product’s behavior and the clearest surviving account of how an analyst was expected to use it. Without a console, it is easy to mistake a database route for the workflow, or a stored field for something an operator could actually see.

I wrote docker-lab/console-web, a vanilla HTML, CSS, and JavaScript single-page application served on loopback by nginx. It speaks to the original Ruby backend through real REST and secure-WebSocket paths. Its structure and names follow the leaked Flex source closely enough that a reviewer can trace a web view back to its MXML, ActionScript manager, and service wrapper.

This is a behavioral reimplementation, not the original console. The Adobe AIR binary was not executed, and the web code was written for this research project. Fidelity comes from multiple forms of comparison: source mapping, mock API contracts, live backend tests, screenshots, and a separate audit of 38 claims extracted from the original operator manuals.

Why not simply run the AIR application?

The source identifies the console as a Spark WindowedApplication and the application descriptor requires the Adobe AIR 15 namespace (rcs-console/src/Console.mxml:1-9 and Console-app.xml:1-30). It is a desktop application with custom transparent window chrome, Flex data grids, MXML skins, ActionScript managers, embedded resource bundles, and numerous SWC dependencies. The descriptor names version label 2015.03.21.01, an initial 1280×768 window, and a minimum 1024×768 size.

That environment is not a practical Linux research target. Reconstructing the matching AIR SDK, Flash Builder behavior, native desktop integration, signed package, and third-party SWCs would add another obsolete executable stack to the lab. Even a successful launch would be hard to automate, inspect, and keep contained. The result could remain dependent on binary libraries whose source and behavior are unclear.

The interface source, however, contains unusually rich specifications. MXML files declare component trees, states, bindings, forms, data-grid columns, visibility rules, and event handlers. ActionScript managers describe collections and push updates. The separate console-library repository wraps the REST endpoints. Locale bundles preserve visible text. Manuals explain the operator’s intended gestures and mental model.

Porting those contracts to the browser made them testable without claiming to recover the original executable. It also created a clean safety boundary. The web application can render descriptions of build and delivery workflows while refusing to submit their backend tasks. That would be much harder to guarantee inside an opaque or partially reconstructed AIR package.

Four evidence sources define fidelity

The port does not use visual resemblance as its only standard. Four evidence classes answer different questions:

Evidence sourceQuestion answeredImportant limitation
MXML view sourceWhat components, states, controls, labels, and handlers were declared?Declaration does not prove every branch worked in a shipped binary
ActionScript libraryHow did managers, REST services, filters, configuration builders, and push patching behave?The library and database snapshots have version skew
Original manualsWhat workflow and visible behavior were documented for operators?Manuals can omit defects and implementation detail
Live 9.2.3 backendDoes the web port exercise the surviving server’s real API, session, storage, and push behavior?The web port is a new client and the paired console source identifies itself as 9.6.0

Mock tests add a fifth, deliberately synthetic layer. They make client state and error paths deterministic. A mock route can confirm that a form emits the intended JSON shape. The old Ruby controller remains untested until a live test reaches it. Conversely, a live test can be slow or stateful, while the mock can cover many UI branches cheaply.

This division is why the book labels console claims WEB PORT, SOURCE, MANUAL, or LAB rather than collapsing them into “observed.” A control found in MXML and rendered by the port is not automatically an observation of the AIR runtime. A REST result from the original server is not automatically proof that the 9.6 console displayed it successfully.

The source was a map, not a file-conversion job

The leaked console application tree contains 506 MXML and 223 ActionScript files; the console-library tree adds one MXML and 352 ActionScript files. The combined source surface is therefore 507 MXML and 575 ActionScript files. Many MXML files are skins, renderers, popup fragments, or variants rather than independent pages. Translating every file into one web file would preserve a directory count but produce a poor application. The port instead preserves functional boundaries and recognizable names.

The principal mapping is:

Flex/AIR conceptWeb counterpartExample
WindowedApplication statesApp login/logout lifecycleConsole.mxmljs/app.js
MXML viewES module with mount/unmount behaviorEvidencesView.mxmlevidence-view.js
DB<Name>.as REST wrapperdomain functions over one HTTP clientconsole library services → js/services/db.js
Manager / ItemManager singletonmanager object with collection and eventscontroller/Manager.asjs/managers/manager.js
Flex EventDispatchershared bus and manager listenerslogin, logout, refresh, and push events
ListCollectionView bindingrecomputed filtered view on manager changeoperation/entity list views
DataGrid and renderersreusable HTML grid plus renderer callbacksjs/components/datagrid.js
ActionBarreusable toolbar componentjs/components/actionbar.js
TitleWindowSaveCancelmodal form shelljs/components/form-popup.js
MXML states and HistoryManagerexplicit section/state machine and snapshotsoperations/ossm.js, core/history.js
Diagrammer graphshand-built SVGconfiguration, entity, frontend, shard graphs
AIR map componentsLeaflet loaded as a plain browser scriptentity and position maps
Resource bundlesgenerated locale JSONsix files under locale/
MXML/CSS skinsproject CSS and copied image assetscss/*.css, img/

There are 93 project JavaScript files and ten CSS files in the finished web tree. That smaller count reflects consolidation of skins and generated service wrappers, not an assertion that every original class has a one-to-one executable equivalent. Where behavior matters, file headers name the source class or MXML view being ported.

All modified and newly written files live under docker-lab. Assets needed by the UI are copied from the read-only archive into the lab tree; the running site never reaches into data/. This keeps the evidence collection immutable and makes the runnable artifact self-contained.

A deliberately plain web stack

The console uses browser-native ES modules, DOM APIs, CSS, SVG, fetch, WebSocket, Web Audio, and local storage. It has no application framework and no production build step. Nginx serves index.html, the source modules, styles, locale files, and copied images directly. Leaflet is the sole substantial vendored browser library, added for maps after the earlier position placeholder was replaced.

This choice was not nostalgia for hand-written JavaScript. It reduced the number of abstractions between the MXML and its port. A reviewer can compare a Flex state transition with an explicit JavaScript method without first unpacking framework lifecycle behavior. Static files also fit the isolated lab well: rebuilding the old backend does not require a contemporary JavaScript toolchain at runtime.

The tradeoff is more project-owned code for grids, forms, popups, state, and SVG layout. The port had to recreate Flex conveniences deliberately. That is why manager semantics, activation and unmount behavior, and tests are more important than the use of any particular web library.

RCS web-port Home view with quick actions and global search
WEB PORT · SYNTHETIC LABHome combines recent items, quick actions, and the GO TO search. The screen is new browser code mapped from the AIR views, populated only with synthetic lab state.

Preserving the application lifecycle

The root AIR application had loggedOut and loggedIn states. On successful login it stored the session, initialized managers, switched the main view, and began dispatching application events. The web App follows the same sequence: load the user’s locale, set the console timezone, connect the push channel, initialize privilege-appropriate managers, create the main shell, capture history state, and dispatch the login event (console-web/js/app.js:1-83).

Section visibility is built from the user’s privilege helpers rather than a hardcoded universal menu. Accounting, Operations, Intelligence, Dashboard, Alerting, System, Audit, and Monitor appear according to the same broad privilege families used by the original client. Initialization loads only the manager collections required by those privileges and waits for them before revealing the shell (views/main/initialization.js:15-83).

Managers are not passive API wrappers. They own domain collections, listen to login/logout/refresh events, emit dataLoaded and change, and patch existing items when push messages arrive (js/managers/manager.js). This reproduces a central Flex behavior: several views can bind to the same live collection and update without re-fetching the page.

Lifecycle fidelity exposed real race conditions. The Intelligence operation tiles, for example, depend on both operation and entity managers. Listening to only the entity manager means a slower operation request can finish after the view rendered, leaving it empty. The port subscribes to both sources because Flex’s bound collection view effectively did the same. The correct web design was found by understanding the original binding semantics, not by copying the visible markup.

REST and push through one origin

The original AIR application could connect directly to the database service. A browser cannot assume that privilege. The old Ruby server sends no CORS headers, its certificate is locally generated, and its REST and secure WebSocket listeners are adjacent but separate ports.

Nginx makes the browser relationship same-origin:

The web HTTP client sends JSON bodies and CGI-style query parameters because those are the encodings the server parser accepts. It decodes JSON or text responses and preserves the original fault policy: a 403 application response forces logout, while a network failure produces a server error (js/core/http.js and js/services/db.js). Binary evidence and task downloads use raw responses rather than JSON conversion.

The WSS client authenticates with the cookie returned by REST, responds to server ping with pong, checks server clock skew, reconnects after closure, and turns push types into application-bus events (js/push/push.js). Logout pushes return the UI to login, matching the same-user behavior described in Chapter 5.

One compatibility detail is almost comically specific. em-websocket 0.3.8 treats the value of the HTTP Connection upgrade header case-sensitively. The proxy must send Connection: "Upgrade"; ordinary lower-case upgrade makes the old server drop the handshake. Nginx also supplies an Origin resembling the one expected by the AIR-era path and disables upstream certificate verification for the lab’s private CA (console-web/nginx.conf:24-38).

Disabling upstream verification is acceptable only inside this isolated single-host compose network. It means nginx encrypts the hop without cryptographically verifying that the backend is the intended peer. The host console binding remains 127.0.0.1:8080, and MongoDB and the worker are not published.

Milestones turned the source tree into testable slices

The console was reconstructed in eight cumulative milestones. Each gate added mock and live coverage rather than postponing integration until the end.

MilestonePrincipal scopeCumulative mockCumulative live
M0login, application shell, localization, REST faults, WSS push177
M1Monitor, Accounting, Audit, Home, manager layer4515
M2Operations/targets/agents drill-down, forms, OSSM state6019
M3evidence grid, filters, per-type renderers/viewers, GridFS8124
M4filesystem, commands, info, sync history, file transfer9430
M5basic/advanced configuration, templates, safe task UI10934
M6Intelligence, Dashboard, Alerting, maps, worker integration12037
M7Frontend, shards, backups, injectors, connectors, polish13544

The last live count comprises 42 milestone tests, one demo-data verification spec, and one named synthetic Worker-ingestion replay. These are the recorded completed gates in console-web/MILESTONES.md; they are not silently represented as a test run performed while drafting this chapter.

The sequence followed product dependencies. Login and push had to work before shared managers. Managers and navigation had to work before nested operations. An agent context was needed before evidence and pseudo-views. Configuration needed collector data and task tracking. Intelligence and alerting needed search, entities, and the worker. System administration needed nearly all the preceding service and form patterns.

M0–M3: establish the analyst loop

M0 began with more than a login form. It established the application shell: custom title bar, section and subsection navigation, status area, clock, alerts, full-screen control, browser-like back and forward history, six locale bundles, the REST fault policy, and WSS authentication. A server-clock warning was carried over because evidence interpretation depends on time. A 403 from any guarded service returns the application to login, preserving the original client’s global invalid-session response.

M1 added the shared administrative collections that made the manager design real. Monitor combines component health, CPU, disk, licence usage, versions, and counters. Accounting implements users and groups with privilege-dependent forms. Audit adds date, enumerated-value, and regular-expression description filters. Home combines recents, quick actions, and a global search that can jump back into Operations. Push messages update users, groups, monitor rows, and counters through the manager layer rather than rebuilding every section.

M2 reconstructed the product’s central hierarchy. Operations contain targets; targets contain factories and deployed agents; agents lead to evidence, configuration, filesystem, commands, device information, synchronization addresses, and transfers. The state machine had to preserve both the selected object and the view mode at each level. Forms also carry real side effects: closing an operation cascades through descendants, moving a target changes its path, and creating a quick agent walks group → operation → target → factory before producing a default configuration.

RCS web-port Operations view showing investigation tiles
WEB PORT · SYNTHETIC LABThe Operations view is the entry point to the operation → target → agent state machine. Tile visibility is derived from group-backed access state.

M3 made stored surveillance data legible. Its grid contains identifiers, acquisition and receipt times, relevance, type, summary, note, report flag, and agent. About twenty type renderers compress different schemas into the summary column. Opening a row can fetch more content and switch to a chat conversation, message body, audio player, image viewer, geographic position, or text view. Keyboard navigation, filter popups, saved presets, type counters, edits, exports, deletion, and target-specific GridFS downloads turn those renderers into an analysis workflow.

The evidence work also enforced provenance discipline. Live fixtures could not be written casually through a current Mongo shell. MongoDB 2.6 binary values inserted that way were misread by the old Moped driver, and an ObjectId-shaped agent identifier did not match the string-valued shard key used by the server. The final seed runs Ruby with Moped inside the database container, uses a string aid, creates minimal statistics required by deletion, and writes binary data to the target’s GridFS bucket. Those details came from testing the old contract, not from visual comparison.

M4–M7: connect analysis to configuration and infrastructure

M4 added views that Flex presented as children of an agent even though they use different backend resources. The filesystem is a lazy tree: root and folder expansion issues path-specific requests, pending rows differ visually from retrieved rows, and push events patch them. Commands combine stored command evidence with a separate queue of future requests. File transfer coordinates a temporary multipart upload, an agent request, and optional execution scheduling. Sync History deliberately remains empty against this database snapshot because the console calls an action the server does not implement.

M5 translated the most complex client-side data structure. Basic configuration is not a collection of independent booleans. Enabling a module generates events, actions, subactions, synchronization behavior, and platform specific defaults. Available modules depend on agent level, platform, and demo status. Collector selection must match the agent’s good routing flag unless a separate hostname feature is licensed. The advanced editor exposes the same configuration as a connected graph and must adjust wires when event or action forms change.

Templates and task downloads completed the safe configuration workflow. Download tracking had to follow tasks created in the current session because the leaked backend’s task-list method indexes its in-memory structure incorrectly and returns an error. The port preserved task creation and polling for in-scope work while refusing build tasks. This is a recurring pattern: recreate the operator contract as far as the backend and research boundary allow, then document the exact stop.

Advanced RCS configuration graph with event, action, and module columns
WEB PORT · SYNTHETIC CONFIGURATIONThe advanced editor makes the configuration graph visible: events feed actions, which control acquisition and synchronization modules. The screenshot is a safe UI fixture, not an operational installer.

M6 joined evidence with higher-level analysis. Intelligence groups entities by operation, renders links, presents geographic positions, and opens a profile with handles and derived “most contacted” views. Dashboard tiles calculate new evidence relative to a baseline stored in item statistics. Alerting joins a user-owned rule, an item path, an evidence type, keywords, relevance, a suppression interval, logs, email behavior, and push notification. The live M6 suite verifies rule creation and rendering but does not ingest an encrypted record. Project notes report a separate synthetic exercise of the full worker chain; a named reproducible test and sanitized trace are still required.

M7 exposed the platform behind the analyst. Frontend renders collectors and anonymizer chains; Backend presents shards and lazy dbStats; Backup combines scheduled jobs with archives; Network Injectors display devices and rules; and Connectors describe local or remote export paths. Privileges and licence flags determine which subsections exist. Several graphs again required SVG-native text and shapes—HTML tables placed inside the SVG namespace simply disappear without a foreignObject bridge.

The milestone boundary is architectural rather than chronological. Later work occasionally corrected an earlier placeholder or assumption: M6 replaced the position viewer’s map placeholder, M7 reused the task manager established in M5, and the manual audit added test hygiene after the functional milestones were green. “Complete” means the present cumulative gate passes, not that each directory froze permanently at its original milestone.

Reconstructing state, not merely pages

The Operations section made the difference between a collection of screens and an application. Its OSSM state machine moves through all operations, one operation, one target, one agent, the agent’s configuration list, a particular configuration, and pseudo-items such as evidence or filesystem. Breadcrumbs, browser-style history, list/table modes, close cascades, and forms all depend on the current state and selected objects.

The web port expresses that state explicitly in operations/ossm.js. Views mount and unmount as states change, mirroring Flex addedToStage and removedFromStage behavior. History captures the current selections rather than only the section name. This was necessary for a Back action to restore an investigation position rather than merely reopen Operations at its root.

Evidence created a different reconstruction problem: polymorphic rendering. The generic nine-column grid delegates summary and detail behavior by type. Chat, messages, calls, microphone recordings, screenshots, camera images, positions, files, passwords, URLs, keylogs, and other types do not share one useful presentation. The port separates row renderers from advanced viewers, then layers filter popups, saved presets, relevance and note edits, export, delete, GridFS attachments, and keyboard navigation over them.

Configuration required rebuilding an editor rather than a form. Basic mode uses the original level × demo × platform gating matrix and regenerates an event/action/module structure through a port of BasicConfigBuilder. Advanced mode renders that structure as an SVG graph with event, action, and module columns and editable pins and forms. Switching from basic to advanced persists the converted configuration—an original behavior sufficiently surprising that the manual audit has to restore its demo fixture afterward.

Intelligence and System views replaced proprietary visual libraries with hand-built SVG. Entity link maps, configuration graphs, anonymizer topology, and backend shard graphs preserve the meaningful nodes, relationships, and interactions rather than the original rendering engine. Leaflet replaces the AIR mapping integration for geographic views and falls back gracefully when map tiles are unavailable in an isolated environment.

These examples show the chosen fidelity level: preserve data, state, visibility, interaction, and server effects; allow the rendering technology to change.

Safety controls are executable requirements

The original console exposed workflows for producing installation packages and other delivery material. Reconstructing their visible shape helps explain the product, but invoking those backend paths is outside this project’s defensive scope.

The M5 Build form therefore renders the original platform and vector tree, options, and warning gate but substitutes a local notification for the Create action. It never submits a task whose type is build. The Frontend Download-Installer control in M7 follows the same rule.

This constraint is tested at two layers. Mock and live browser tests observe outgoing requests and assert that clicking the controls does not create a build task. The mock API also rejects a direct type: build task request. The controls are not merely disabled by CSS; their prohibited server effect is an explicit regression condition (tests/specs/m5.mock.spec.js:201-281 and m5.live.spec.js:131-165).

Other task types needed for backend administration or evidence handling remain available when they are within the lab scope, but tests use synthetic objects and isolated users. No core, implant, or exploit code is copied into a runnable path. Visual coverage is not authorization to execute the workflow it depicts.

Two test systems answer different questions

The final validation architecture is deliberately split:

                         source comparison
                 MXML + ActionScript + manuals
                              |
                              v
                    console-web static SPA
                       /              \
                      /                \
           mock profile :8081       live profile :8080
          Playwright route mocks       nginx same-origin proxy
            fixtures + failures        /api/          /wss/
                    |                    |               |
        deterministic UI state       rcs-db REST     rcs-db push
                                             \       /
                                          MongoDB + worker

The mock profile starts a local static server on port 8081 and intercepts routes in Playwright. It blocks WSS so a running live stack cannot accidentally receive a fixture cookie. Mock tests can force failure responses, exact push events, empty states, and rare form combinations without persistent backend setup.

The live profile opens the compose-served site on port 8080 and uses real REST, WSS, MongoDB, and worker services. Tests create unique full-privilege users, then perform CRUD and workflow checks through the same interfaces as the page. Helper requests use a separate Playwright request context because page.request shares cookies and can overwrite the UI session. Unique names and ordered cleanup avoid poisoning later runs with the old server’s asynchronous access control behavior.

The viewport is fixed at 1280×768, matching the original application descriptor’s initial dimensions. Screenshots exist for each major view, while failed tests retain traces and screenshots. Visual artifacts supplement behavioral assertions; they do not replace them.

Synthetic data and what it proves

The console needs enough linked data to expose its workflow. The demo seed creates two operations, three deployed synthetic agents, about 74 evidence documents spread across roughly two weeks, entities and links, alert logs, dashboard pins, and representative system objects. It uses the authentic first-sync status route to turn factories into agent records but never starts an implant.

Most evidence rows are inserted directly through Moped inside the database container. That preserves the era BSON behavior and target-scoped collection shape needed by the server. Their provenance begins at storage, not at collection, transport, or worker decoding. A separate named live test sends a synthetic encrypted DEVICE record through the internal Worker and verifies storage, alert queue processing, an alert log, and a push. Chapter 7 keeps these provenance classes separate.

Demo alert logs are fabricated in the stored dispatcher shape because direct evidence insertion bypasses Evidence#enqueue. Map views may load public Open Street Map tiles when network access exists, but display an offline placeholder when it does not. Screenshots of the demo are therefore illustrations of the reconstructed workflow, not records from a surveillance operation.

Test hygiene is part of validity. Alerts belong to users, dashboard identifiers are captured in the login-time session state, and item visibility is propagated through groups. The seed and tests must create relationships in the order the old backend expects. When a test changes a persistent configuration, it restores the original fixture before completing.

The manuals became executable claims

Source correspondence does not answer whether an operator reading the manual would recognize the result. The project therefore added a separate manual-versus-UI audit.

The four relevant RCS 9 manuals are image-only PDFs covering analyst, administrator, system-administrator, and technician work. The audit rasterized and OCRed 479 PDF pages, then turned selected statements into 38 concrete claims. The cache also carries four combined text files, one per manual. Each claim records a manual and PDF page and implements a Playwright check against the running console. Examples include the login fields, operation double-click drill-down, default 24-hour evidence window, evidence type list, filesystem toolbar, intelligence views, dashboard tile fields, alert rule form, audit filters, anonymizer topology, shard graph, and basic and advanced configuration editors.

The last recorded result is 38 of 38 claims matching. Three claims—application shell, operation drill-down, and System sections—are designated critical. The manual audit is opt-in rather than part of the normal 44-test live gate because it requires the full demo seed, a long single-page journey, and about a minute of dedicated execution.

The audit produced findings even when it passed. Evidence-grid headers shorten “Relevance” and “Report” to “Rel” and “Rep.” The long-lived advanced SVG check needs a fresh browser page. Changing a basic configuration to advanced mode authentically saves the conversion, so the claim must restore the seeded configuration. Leak-era 404s such as the missing sync_history action remain visible against the old backend.

This is the right meaning of 38/38: all selected, cited claims match after the known test mechanics are handled. It does not show that all 479 manual pages were tested or that every original MXML file and runtime branch is equivalent.

Bugs that became documentation

The most valuable failures were often evidence of a hidden original contract. They were recorded in AGENTS.md, MILESTONES.md, and the manual-audit handoff so a future maintainer does not “fix” the port away from the source.

The same-user login race is one example. Early live tests reused admin, then pages logged out apparently at random. Source review showed that every login invalidates the previous session and queues a user-addressed push. The solution was not retry logic; it was a unique user per test and a separate helper request context.

Group cleanup exposed another asynchronous contract. Destroying a group first can trigger a deferred access-control rebuild that empties an operation’s user list. A later operation deletion then fails its own access check and leaves an orphan. Tests now delete operations before groups and purge known leftovers before a run. That cleanup ordering documents backend behavior relevant to incident response as well as testing.

Configuration tests found that adding a config to a factory replaces its existing configuration, while deployed agents accumulate configurations. The basic editor also removes modules unsupported by a platform and restores missing supported modules. A superficially simpler form would have produced JSON accepted by the server but unlike the original console’s output.

Dashboard testing showed that the session user is effectively a login-time client snapshot and the server never emits a user push after updating dashboard_ids. The original client patched its own state after saving. The web port follows that pattern rather than fetching invented server behavior.

Entity handles revealed a wire-shape mismatch: the REST controller expects flat name, type, handle, and handle_id parameters. An intuitively nested object reaches downcase as a hash and produces a server error. The mock was changed to mirror the flat shape so it could no longer hide a live failure.

The manual audit added a different category of finding. A ten-minute single page accumulated enough prior DOM and SVG work that the advanced graph could lose its browser context. Giving that claim a fresh page required a second user because reusing the first would trigger the same-user logout behavior. The test mechanic is not an original product feature, but the workaround still had to respect original session rules.

These discoveries justify the extensive handoff documentation. A green suite without its failure history tells a maintainer what works. The recorded traps explain why apparently cleaner alternatives do not.

Preserved quirks and fidelity exceptions

Some apparent defects were retained because correcting them would misrepresent the source pairing. Others are genuine differences introduced by the web environment.

AreaStatus in the web reconstructionFidelity interpretation
Original executableAIR binary is not runNew client; no binary-equivalence claim
RenderingHTML/CSS/SVG/Leaflet replace Spark, proprietary graphing, and AIR mapsBehavioral and structural, not pixel-level fidelity
Sync HistoryEmpty against 9.2.3 because /evidence/sync_history is absentPreserved cross-version gap, not mocked into success live
Task listBackend GET /task is broken; DownloadManager tracks tasks created in its sessionPreserved server limitation with client workaround
Dashboard pinsClient patches its login-time user state after update because server sends no user pushMatches original console-side behavior
Evidence fixture originMost demo rows are direct Moped insertsProves UI/storage contract, not collection
MapsPublic Leaflet tiles can be unavailableOffline fallback differs visually but preserves position data
Evidence headers“Rel” and “Rep” abbreviate manual labelsRecorded cosmetic exception
Build and installer actionsVisible but render-only; no build task is sentIntentional defensive divergence
Upstream TLSNginx does not verify the lab backend’s private certificateLab-only deployment compromise, not original AIR behavior
Session testingUnique users replace repeated administrator loginTest isolation required by original one-session policy

The port also exposes a small window.__rcs handle for Playwright. It reveals current application state, session, section, and mounted app object so tests can inspect state without brittle DOM scraping. That interface is research instrumentation and had no AIR equivalent.

What the reconstructed console proves

The console establishes that the surviving backend can support a coherent operator workflow: authenticate, initialize privilege-specific sections, navigate operations and targets, render stored evidence, edit configuration, manage entities and alerts, receive pushes, and inspect system state. Its live tests exercise original REST, WSS, MongoDB, and selected worker behavior. Its manual audit shows that the selected visible workflow aligns with contemporary documentation.

The result stops short of the AIR runtime. It cannot tell us whether every customer used this console build or whether a 9.6 client and 9.2.3 database were ever an approved production pairing. Proprietary graphing pixels, native window behavior, and peripheral libraries are outside the reconstruction. The web port also deliberately refuses the operational build paths the original interface offered.

Most importantly, the console changes what can be asked of the archive. We can now observe how backend records became an analyst’s hierarchy, how session and push quirks affected real work, how evidence types shaped investigation, and how administrative trust appeared to operators. The next chapters use that view carefully: the web port demonstrates the client contract, while source, manual, and live-backend provenance remain visible for every consequential claim.

Sources and evidence

↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life