Skip to content

About

Self-hosted database migration assessment toolkit with a .NET API, React portal, Docker discovery agents, and AI-assisted reporting.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

DB Migration Assessment Toolkit

A self-hosted portal for assessing database estates before migration to the cloud. Lightweight Docker Discovery Agents are deployed inside customer networks, collect findings from source databases, and upload signed result bundles to the portal for AI-assisted analysis and report generation.

Project site: https://prjelesi.github.io/DBMigrationAssist/

The static project site introduces the assessment workflow, engine validation status, network boundaries, Azure deployment, and local setup. Its workspace and evidence previews are illustrative, not live assessments. It opens in a light theme, with an optional dark-mode toggle. Changes under site/ deploy to GitHub Pages when merged to main.

Licensed under MIT. See contribution guidance, the security policy, and third-party notices. The toolkit is community-maintained; production hardening and support remain the operator's responsibility.


Table of contents


Architecture

  Customer network                    Portal (Docker / Azure Container Apps)
  ─────────────────                   ─────────────────────────────────────
  ┌────────────────┐  HTTPS + HMAC    ┌──────────┐       ┌────────────────┐
  │ Agent          │ ──────────────►  │ API      │ ────► │ PostgreSQL     │
  │                │                  │ :5000    │       │ :5432          │
  │ Plugins:       │                  └────▲─────┘       └───────▲────────┘
  │ · postgresql   │                       │                     │
  │ · oracle       │                  ┌────┴─────┐         ┌─────┴────┐
  │ · sqlserver    │                  │ Frontend │         │ Worker   │
  │ · mysql        │                  │ :3000    │         │          │
  │ · mongodb      │                  └──────────┘         └──────────┘
  │ · cassandra    │
  │ · sybase       │
  └────────────────┘
Container Purpose
api REST API — portal backend, job dispatch, evidence, AI enrichment
worker Background services — heartbeat monitor, job timeout, AI auto-enrichment
frontend React portal UI served via nginx
postgres PostgreSQL 16 — all portal data
local-agent Platform-managed Discovery Agent for local demonstrations and assessments

Customer-hosted Discovery Agents use the same agent image and run inside the customer network, where they poll for jobs and execute database plugins over outbound HTTPS.


Fictional customer example

The Banana Inc Oracle migration example provides an import-ready database inventory, application dependency register, network and security designs, a PostgreSQL-first target decision framework, and a follow-up assessment plan. Its presenter runbook provides a timed portal walkthrough, expected import checks, dependency-map entries, migration waves, and an optional runnable Oracle extension. All customer and workload details in the example are fictional.


Prerequisites

Tool Version
Docker Desktop (or Docker Engine + Compose) 24 +
.NET SDK 8.0 (build only)
Node.js + npm 24 LTS (build only)
PowerShell 7 +

Note: The Docker images are built from pre-compiled output. You must run build.ps1 on a machine with the SDKs before running docker compose build.


Quick start — local

# 1. Restore dependencies and build (requires .NET 8 SDK + Node 24 LTS)
git clone https://github.com/prjelesi/DBMigrationAssist.git
Set-Location .\DBMigrationAssist
dotnet restore src/AssessmentToolkit.sln
Push-Location .\src\frontend
npm ci
Pop-Location
.\build.ps1

# 2. Create local configuration and set a strong development password
Copy-Item .env.example .env
notepad .env

# 3. Build Docker images
docker compose build

# 4. Start the stack
docker compose up -d

# 5. Open the portal
start http://localhost:3000

The API is available at http://localhost:5000 (Swagger UI at /swagger).

On first start, EF Core migrations run automatically and all database schemas are created.

Portal authentication is required in the local Compose environment. The first application administrator is provisioned from APP_ADMIN_EMAIL and APP_ADMIN_PASSWORD; background worker calls use the separate WORKER_SERVICE_TOKEN. Discovery Agent endpoints remain protected by their PSK/HMAC signatures.


Portal walkthrough

Section Route Description
Dashboard / Counts of projects, Discovery Agents, databases
System /system Health status, AI configuration, system config
Projects /projects Create migration projects; project owners and administrators can permanently delete them
Environments /environments DEV / UAT / PROD environment rings
DB Inventory /inventory Register source databases with engine, host, criticality
Discovery Agents /agents Register Discovery Agents, dispatch jobs, view findings
Communication Map /communication-map ReactFlow graph of database dependencies
Portfolio /portfolio Group databases into migration waves
Questionnaire /questionnaire Run versioned questionnaires for the whole project or a specific database
Evidence /evidence Upload assessment reports and questionnaire supporting documents
Reports /reports AI-generated migration assessment reports
AI Settings /ai-settings Configure Microsoft Foundry/Azure OpenAI and administer report instructions

Offline database assessment

Use this workflow when a Discovery Agent cannot reach a database:

  1. Create the database definition in DB Inventory.
  2. In that database row, select Download offline assessment script and transfer the generated PowerShell script to the customer.
  3. Install the database's native command-line client on the execution host, then run the script with a read-only database account:
pwsh ./collect-<database>-assessment.ps1 -Username <read-only-user>
  1. Enter the password at the secure prompt. The script writes an assessment JSON file beside the script and removes temporary credential state when collection finishes.
  2. Have the customer return the JSON file. In the same database row, select Upload assessment result and upload it.
Engine Required client Credential handling
PostgreSQL psql Temporary PGPASSWORD environment variable
Oracle sqlplus Login sent through standard input to /nolog
SQL Server sqlcmd Temporary SQLCMDPASSWORD environment variable
MySQL / MariaDB mysql Temporary MYSQL_PWD environment variable
MongoDB mongosh Native hidden password prompt; no password argument
Cassandra cqlsh Permission-restricted temporary credentials file, deleted on exit
Sybase ASE isql Native password prompt; no password argument

The offline-2.0 collector stores findings under findingJson.sections. Every metadata query has an independent success or failed status, so a denied optional catalog query does not discard sections that completed successfully. PostgreSQL, Oracle, and MySQL collectors also include engine-specific migration signals such as missing primary keys, problematic data types, wrapped or unsupported objects, LOB and redo sizing, non-InnoDB tables, partitions, authentication plugin counts, and buffer-pool occupancy. The collector does not query application table rows, and section errors redact the collected password.

The collector embeds the inventory database ID and engine plugin. The portal rejects a result uploaded against a different database, an incompatible engine, an unsupported schema version, a non-JSON file, or a file larger than 10 MB. Uploaded results have no Discovery Agent identity and are stored as unverified assessments until reviewed. The result is not cryptographically signed, so controlled transfer and collector integrity verification remain part of the customer's operating process.


Questionnaire templates

Questionnaires are defined as versioned JSON files in src/backend/AssessmentToolkit.Api/QuestionnaireTemplates/. The portal includes a generic baseline plus supplemental templates for Oracle, PostgreSQL, SQL Server, and MySQL. Use Sync JSON templates on the Questionnaire page to import new template versions; existing versions and their responses are preserved.

After selecting a project, choose Entire project for shared migration context or select one inventory database for workload-specific answers. A project can have one response per template at project scope and one response per template for each database. Starting the same template and scope resumes the existing response. Supporting attachments inherit the selected database scope. Project reports consume project-wide responses; database reports combine those shared responses with the responses for the selected database.

Each question supports Text, Textarea, Select, MultiSelect, Boolean, or Number. The following optional capabilities are controlled per question:

{
  "text": "Which database features are business-critical?",
  "type": "MultiSelect",
  "required": true,
  "allowCustomAnswer": true,
  "allowAttachments": true,
  "options": ["RAC", "Data Guard", "GoldenGate", "None"]
}
  • allowCustomAnswer adds an Other / custom answer field and is valid only for Select and MultiSelect questions.
  • allowAttachments lets the respondent upload supporting documents up to 50 MB. Files remain visible in the project Evidence Library and are linked to the response and question.
  • An attachment satisfies a required attachment-enabled question when no text answer is supplied.
  • Documents can be downloaded at any time, but answers and attachments become read-only after submission.

Increment version when changing a template that may already have responses. Do not replace a synchronized version in place.

Deleting projects

Project owners and application administrators can permanently delete a project from the Projects page. Use the trash action in the project row and confirm the named project. Deletion removes the project's environments, database inventory, Discovery Agents, assessment jobs and results, questionnaires, evidence files, communication-map edges, portfolios, generated reports, and memberships. This action cannot be undone and does not delete the parent customer or independent audit history.


Deploying Discovery Agents

The Discovery Agent is a .NET 8 background service that runs inside the customer network alongside the source databases. It polls the portal for jobs, runs the appropriate plugin against the target database, and uploads a signed result bundle over HTTPS.

Supported plugins

Plugin ID Source database Default port Status
postgresql PostgreSQL 5432 Supported
oracle Oracle Database 1521 Supported
sqlserver Microsoft SQL Server 1433 Supported
mysql MySQL / MariaDB 3306 Supported
mongodb MongoDB 27017 Supported
cassandra Apache Cassandra 9042 Supported
sybase SAP/Sybase ASE 5000 Supported

The connected plugins collect server, topology, schema, sizing and compatibility metadata. They do not read application-row or document content. Optional metadata sections report a section-level error when the assessment account lacks permission instead of failing the complete assessment.

1. Register a Discovery Agent in the portal

  1. Navigate to Discovery Agents and select a project.
  2. Click + Register Discovery Agent.
  3. Fill in:
  • Discovery Agent Name — a descriptive label (e.g. Prod-Oracle-Discovery-1)
  • Allowed Plugins — select only the engines this Discovery Agent can reach: postgresql, oracle, sqlserver, mysql, mongodb, cassandra, and/or sybase
  • PSK Mode — System generated (recommended) or User supplied
  1. Click Register Discovery Agent.
  2. A bootstrap package is shown once — copy or download it immediately:
{
  "agentId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "backendUrl": "https://your-portal-api-url",
  "psk": "your-pre-shared-key",
  "allowedPlugins": ["postgresql", "oracle", "sqlserver", "mysql", "mongodb", "cassandra", "sybase"],
  "issuedAt": "2026-07-28T10:00:00Z"
}

Security: The PSK is shown once and never stored in recoverable form. Store it in a secrets manager or environment variable before closing the dialog.


2. Choose a deployment method

Method Best for
Docker run Single-machine quick deployment
Docker Compose profile Local PoC alongside the portal
bootstrap.json file Air-gapped or restricted environments
Kubernetes Production / enterprise deployments

Build the agent image

The agent Dockerfile copies pre-published .NET output. Build and publish before creating the image:

cd c:\github\DBMigrationReady
.\build.ps1
docker build -t dbmigrationready-agent:latest .\src\agent\AssessmentToolkit.Agent

For a shared registry, tag and push the image, then use that registry name in the deployment examples:

docker tag dbmigrationready-agent:latest registry.example.com/dbmigrationready-agent:1.0
docker push registry.example.com/dbmigrationready-agent:1.0

3. Docker run (simplest)

Pull and run the agent image with environment variables from the bootstrap package:

docker run -d \
  --name dbmr-agent \
  --restart unless-stopped \
  -e AGENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  -e AGENT_PSK="your-pre-shared-key" \
  -e AGENT_BACKEND_URL="https://your-portal-api-url" \
  -e AGENT_PLUGINS="postgresql,oracle,sqlserver,mysql,mongodb,cassandra,sybase" \
  dbmigrationready-agent:latest

Environment variables:

Variable Required Description
AGENT_ID ✅ Agent GUID from the bootstrap package
AGENT_PSK ✅ Pre-shared key from the bootstrap package
AGENT_BACKEND_URL ✅ Base URL of the portal API (no trailing slash)
AGENT_PLUGINS optional Comma-separated implemented plugin IDs. Default: postgresql,oracle
AGENT_BOOTSTRAP_PATH optional Path to a bootstrap.json file (overrides env vars above)
AGENT_DATABASES_PATH optional Path to the local database credential file. Default: databases.json

Network: The agent only makes outbound HTTPS calls to AGENT_BACKEND_URL. No inbound ports are required. Ensure outbound TCP/443 is open from the agent host to the portal.


4. Platform Local Agent

The default Docker Compose stack includes one Platform Local Agent. The API provisions its identity and PSK into a private Docker volume, and the agent reuses that identity across container restarts. It appears in Discovery Agents for every project and supports all seven implemented plugins.

docker compose up -d

Use Compose service names such as postgres for databases in the same Docker network. Use host.docker.internal for a database running directly on the Docker host. Ad-hoc credentials can be supplied when dispatching a job; for locally held credentials, see local-agent/databases.example.json.

5. Docker Compose profile (isolated testing)

The docker-compose.yml retains an agent-dev service under the agent profile for isolated tests with temporary or manually registered identities. To use it:

  1. Edit docker-compose.yml and fill in AGENT_ID and AGENT_PSK from the bootstrap package:
agent-dev:
  environment:
    AGENT_BACKEND_URL: "http://api:8080"
    AGENT_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"   # ← paste here
    AGENT_PSK: "your-pre-shared-key"                    # ← paste here
    AGENT_PLUGINS: "postgresql,oracle,sqlserver,mysql,mongodb,cassandra,sybase"
  1. Start with the agent profile:
docker compose --profile agent up -d

Note: AGENT_BACKEND_URL uses the internal Docker network name api when running alongside the portal stack.


6. bootstrap.json file

For air-gapped environments where injecting env vars is inconvenient, save the bootstrap package as bootstrap.json and mount it into the container:

# Save the bootstrap package to a file
cat > bootstrap.json << 'EOF'
{
  "agentId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "backendUrl": "https://your-portal-api-url",
  "psk": "your-pre-shared-key",
  "allowedPlugins": ["postgresql", "oracle", "sqlserver", "mysql", "mongodb", "cassandra", "sybase"]
}
EOF

docker run -d \
  --name dbmr-agent \
  --restart unless-stopped \
  -v $(pwd)/bootstrap.json:/app/bootstrap.json:ro \
  dbmigrationready-agent:latest

The agent looks for bootstrap.json in /app/ by default, or at the path set in AGENT_BOOTSTRAP_PATH.


7. Kubernetes / bare-metal

Create a Kubernetes Secret from the bootstrap values and reference it in a Deployment:

apiVersion: v1
kind: Secret
metadata:
  name: dbmr-agent-bootstrap
type: Opaque
stringData:
  AGENT_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  AGENT_PSK: "your-pre-shared-key"
  AGENT_BACKEND_URL: "https://your-portal-api-url"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dbmr-agent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dbmr-agent
  template:
    metadata:
      labels:
        app: dbmr-agent
    spec:
      containers:
        - name: agent
          image: dbmigrationready-agent:latest
          envFrom:
            - secretRef:
                name: dbmr-agent-bootstrap
          env:
            - name: AGENT_PLUGINS
              value: "postgresql,oracle,sqlserver,mysql,mongodb,cassandra,sybase"

Each registered agent should have its own bootstrap package. Running multiple replicas of the same agent GUID is not supported.

For bare-metal Linux, copy the contents of src/agent/AssessmentToolkit.Agent/publish to the target host, install the .NET 8 runtime, set the bootstrap environment variables, and run:

cd /opt/dbmr-agent
export AGENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export AGENT_PSK="your-pre-shared-key"
export AGENT_BACKEND_URL="https://your-portal-api-url"
export AGENT_PLUGINS="postgresql,oracle,sqlserver,mysql,mongodb,cassandra,sybase"
dotnet AssessmentToolkit.Agent.dll

For a persistent service, place these variables in a root-readable environment file and configure systemd with WorkingDirectory=/opt/dbmr-agent, ExecStart=/usr/bin/dotnet /opt/dbmr-agent/AssessmentToolkit.Agent.dll, and Restart=always. Do not put the PSK in a world-readable unit file.


Discovery Agent user instructions

Complete these steps after registering and deploying a Discovery Agent.

Configure database credentials

Use a dedicated read-only database account. The preferred production approach is to keep credentials on the Discovery Agent host instead of entering them for every portal job.

  1. In DB Inventory, identify the database to assess. Retrieve its databaseId from GET /api/databases?projectId=<project-guid>; for a local portal, use http://localhost:5000/api/databases?projectId=<project-guid>.
  2. On the Discovery Agent host, create databases.json:
{
  "databases": [
    {
      "databaseId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "pluginId": "postgresql",
      "host": "db.internal",
      "port": 5432,
      "database": "applicationdb",
      "username": "assessment_reader",
      "password": "replace-with-secret"
    }
  ]
}
  1. Mount the file read-only when starting the agent:
docker run -d \
  --name dbmr-agent \
  --restart unless-stopped \
  -v $(pwd)/bootstrap.json:/app/bootstrap.json:ro \
  -v $(pwd)/databases.json:/app/databases.json:ro \
  dbmigrationready-agent:latest

The databaseId must match the selected portal inventory record. Environment variables can be used instead of a file:

AGENT_DB_0_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AGENT_DB_0_PLUGIN=postgresql
AGENT_DB_0_HOST=db.internal
AGENT_DB_0_PORT=5432
AGENT_DB_0_NAME=applicationdb
AGENT_DB_0_USER=assessment_reader
AGENT_DB_0_PASSWORD=replace-with-secret

Add more profiles with AGENT_DB_1_*, AGENT_DB_2_*, and so on. Numbering must be contiguous from zero. Environment profiles override file entries with the same database ID.

For a one-time assessment, credentials can instead be entered in the Dispatch Job dialog. They are sent to the Discovery Agent in the job payload; use TLS and a dedicated read-only account.

Start and verify the Discovery Agent

docker start dbmr-agent
docker logs -f dbmr-agent

A successful startup logs the agent ID, backend URL, and number of local database profiles loaded. Within about 30 seconds, the portal Discovery Agents page should show a recent heartbeat and move the Discovery Agent to an active state.

Dispatch an assessment

  1. Open Discovery Agents and select the project.
  2. Confirm the Discovery Agent has a recent heartbeat and the required plugin is listed.
  3. Select Dispatch Job.
  4. Choose a database from inventory to use its matching local credential profile, or enter the host, port, database/service name, username, and password manually.
  5. Confirm the plugin matches the database engine, then select Dispatch.
  6. Open Job History. The job progresses from pending/assigned to running and then completed or failed. Discovery Agents poll every 30 seconds, so pickup is not immediate.

View findings

In Job History, select the finding count for a completed job. The portal shows raw plugin findings and, when AI is configured, migration-path enrichment, risks, complexity, and remediation steps.

Troubleshooting

Symptom Check
Agent exits immediately Verify AGENT_ID, AGENT_PSK, and AGENT_BACKEND_URL, or validate the mounted bootstrap.json.
Agent remains registered/offline Check docker logs, DNS/TLS access to the API, host clock synchronization, and outbound TCP/443.
Job stays pending Confirm the agent is running, its heartbeat is current, and the job plugin is in the agent's allowed plugin list.
Authentication fails Use the PSK from the one-time bootstrap package and synchronize the agent clock; signed requests allow a five-minute skew.
Database connection fails Test name resolution and the database port from the agent host; verify the read-only account and database/service name.
Local credentials are ignored Ensure the job uses an inventory database and its databaseId exactly matches the local profile. Restart after changing local files or environment variables.
Plugin is skipped Enable the matching implemented plugin ID and restart the agent: postgresql, oracle, sqlserver, mysql, mongodb, cassandra, or sybase.

Testing

Run the backend and frontend unit tests:

dotnet test .\src\tests\AssessmentToolkit.Api.Tests\AssessmentToolkit.Api.Tests.csproj

Push-Location .\src\frontend
npm test
Pop-Location

The backend suite covers administrative authorization, agent HMAC and result submission, report-contract validation, assessment-result selection, and Oracle evidence parsing/report shaping. The frontend Vitest suite covers persisted-report validation and unavailable-state rendering. These are focused suites, not comprehensive application regression coverage.

PostgreSQL connected-agent test

The PostgreSQL smoke test creates an isolated customer, project, agent, and job; starts the real agent container; assesses the Compose PostgreSQL instance; and verifies that a finding containing server_version is persisted.

# Use the disposable Compose instance's configured credentials.
$env:APP_ADMIN_EMAIL = 'admin@example.com'
$env:APP_ADMIN_PASSWORD = Read-Host 'Local administrator password' -MaskInput
$env:POSTGRES_PASSWORD = Read-Host 'Local PostgreSQL password' -MaskInput

# Build current binaries and images, then run the test
.\tests\AgentPostgresE2E.ps1 -BuildImages

# Re-run against already-built images
.\tests\AgentPostgresE2E.ps1

The temporary agent container is removed automatically. Test records use unique e2e-* names so repeated runs do not conflict.

Worker database smoke test

After building the current Worker image and starting a disposable Compose stack with API migrations complete, run:

docker compose build worker
.\tests\WorkerPostgresSmoke.ps1

This starts a separate temporary Worker container and requires a successful PostgreSQL agents/jobs status query, not merely a running process. Runtime assembly-loading errors or logged failures fail the test. The temporary Worker is removed automatically; the existing Worker container is not restarted. CI runs this check after the connected-agent test.


Database account requirements

The agent uses a read-only dedicated account. Never use sa, SYS, or a DBA account.

PostgreSQL

-- Create a dedicated assessment user
CREATE USER assessment_reader WITH PASSWORD 'strong-password-here';

-- Grant read access to all existing tables
GRANT CONNECT ON DATABASE your_database TO assessment_reader;
GRANT USAGE ON SCHEMA public TO assessment_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO assessment_reader;

-- Ensure new tables are also accessible
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO assessment_reader;

The agent also queries system views (pg_database, pg_extension, information_schema) which are readable by any login role.

Oracle

-- Create a dedicated assessment user
CREATE USER assessment_reader IDENTIFIED BY "strong-password-here";
GRANT CREATE SESSION TO assessment_reader;

-- System views required by all collections
GRANT SELECT ON V_$VERSION            TO assessment_reader;
GRANT SELECT ON V_$DATABASE           TO assessment_reader;
GRANT SELECT ON V_$INSTANCE           TO assessment_reader;
GRANT SELECT ON GV_$INSTANCE          TO assessment_reader;
GRANT SELECT ON V_$SESSION            TO assessment_reader;
GRANT SELECT ON V_$PARAMETER          TO assessment_reader;
GRANT SELECT ON V_$SGAINFO            TO assessment_reader;
GRANT SELECT ON V_$SYSTEM_EVENT       TO assessment_reader;
GRANT SELECT ON V_$LOG                TO assessment_reader;
GRANT SELECT ON V_$ARCHIVED_LOG       TO assessment_reader;
GRANT SELECT ON V_$PDBS               TO assessment_reader;

-- DBA views required for schema/object inventory (recommended)
GRANT SELECT ON DBA_USERS TO assessment_reader;
GRANT SELECT ON DBA_OBJECTS TO assessment_reader;
GRANT SELECT ON DBA_DATA_FILES TO assessment_reader;
GRANT SELECT ON DBA_TEMP_FILES TO assessment_reader;
GRANT SELECT ON DBA_FREE_SPACE TO assessment_reader;
GRANT SELECT ON DBA_TABLESPACES TO assessment_reader;
GRANT SELECT ON DBA_TEMP_FREE_SPACE TO assessment_reader;
GRANT SELECT ON DBA_SEGMENTS TO assessment_reader;
GRANT SELECT ON DBA_PROFILES TO assessment_reader;
GRANT SELECT ON DBA_SYNONYMS TO assessment_reader;
GRANT SELECT ON DBA_SQL_PROFILES TO assessment_reader;
GRANT SELECT ON DBA_SQL_PLAN_BASELINES TO assessment_reader;
GRANT SELECT_CATALOG_ROLE TO assessment_reader;       -- convenience role
GRANT SELECT ON SYS.AUX_STATS$ TO assessment_reader;

The plugin treats optional catalog and platform views independently. Missing privileges are recorded in section_errors with the Oracle error code while other sections continue. AWR is intentionally not queried by the agent; upload a database-scoped AWR text or HTML report as evidence when licensed performance analysis is required. The evidence parser extracts database identity, host capacity, snapshot duration, DB time, load-profile rates and top timed events before those values are supplied to report generation. Exadata and ASM sections require access to V_$CELL_STATE and V_$ASM_DISKGROUP respectively and are reported as unavailable when those views are not accessible.

SQL Server

USE [your_database];
CREATE USER [assessment_reader] FOR LOGIN [assessment_reader];
GRANT CONNECT TO [assessment_reader];
GRANT VIEW DATABASE STATE TO [assessment_reader];
GRANT VIEW DEFINITION TO [assessment_reader];
GRANT SELECT TO [assessment_reader];

Create the login at the server level using your organization's password and identity policy. Grant only the target databases required for assessment.

MySQL / MariaDB

CREATE USER 'assessment_reader'@'agent-host-or-subnet'
  IDENTIFIED BY 'strong-password-here';
GRANT SELECT, REPLICATION CLIENT, PROCESS ON *.*
  TO 'assessment_reader'@'agent-host-or-subnet';

Restrict the account host to the agent host or subnet. SELECT provides metadata access; REPLICATION CLIENT and PROCESS enable replication and connection findings.

MongoDB

Use a dedicated user with metadata privileges for the target database. It must be able to run listDatabases, listCollections, listIndexes, dbStats, collStats, and buildInfo. Cluster-level sections such as replica-set status, sharding and feature compatibility are optional; grant the corresponding cluster-monitoring privileges only when those findings are required. The plugin estimates collection counts but never reads application documents.

Cassandra

Use a dedicated login with permission to connect and read metadata from system and system_schema. Grant access to system_distributed only when your Cassandra version or topology requires it for operational metadata. The plugin does not query application keyspace rows.

SAP/Sybase ASE

Use a dedicated login with catalog read access in the assessed database. Access to the master database catalogs is recommended for server-wide database, device and remote-server findings; when it is unavailable, those optional sections report permission errors while database-local collection continues. Do not grant sa_role solely for assessment.


Enabling AI features

AI enrichment and report generation use Azure OpenAI (or any Azure AI Foundry deployment). Without AI configuration the portal works fully — AI features simply show a warning banner.

Option A — Environment variables (recommended for production)

Add to docker-compose.yml under the api service:

environment:
  AiFoundry__Endpoint: "https://<your-resource>.openai.azure.com/"
  AiFoundry__Deployment: "gpt-4o"
  # API key — never commit to source control
  AZURE_OPENAI_API_KEY: "<your-api-key>"

Option B — Portal UI

  1. Go to AI Settings (/ai-settings).
  2. Enter the Azure OpenAI endpoint, deployment name, and API key under Active Connection.
  3. Select Save Configuration. The key is encrypted with ASP.NET Core Data Protection before it is stored and is never returned to the browser.
  4. Select Test Connection to verify that the configured deployment can answer a model request.

Saving configuration and proving connectivity are separate states:

Status Meaning
Not configured Endpoint or credential is missing.
Not tested / Configured Configuration loaded, but no successful model request has been made since startup or the last configuration change. The automatic startup check normally clears this state within a few seconds.
Connected The latest manual or automatic connection test returned a model response.
Connection failed / Error The latest test could not get a model response. Check endpoint, deployment, key, and network access.

For Docker Compose, the Data Protection key ring is persisted in the data_protection_keys named volume. If deploying the API another way, set DataProtection__KeysPath to durable storage shared across API restarts. Back up that key ring with the database; without it, stored API credentials cannot be decrypted.

The API tests configured Foundry connectivity five seconds after startup and every 15 minutes afterward. Override the defaults with AiFoundry__ConnectivityCheckStartupDelaySeconds and AiFoundry__ConnectivityCheckIntervalMinutes. The System and AI Settings pages poll the resulting status automatically. Manual Test Connection remains available for immediate verification.

The adapter retries explicit model-parameter rejections using max_completion_tokens instead of max_tokens, or the model's default temperature instead of a custom value. Deployment names can be arbitrary; no model-name naming convention is required. Connection-test responses include actionable HTTP failure details; full diagnostic details remain in the API logs.

Verifying AI is active

curl http://localhost:5000/api/ai/status
# {"available":true,"connectionStatus":"Not tested","message":"Configuration loaded. Test the connection to verify model access.",...}

Wait for the automatic startup check, use Test Connection in AI Settings, or call:

curl -X POST http://localhost:5000/api/ai/test
# {"succeeded":true,"message":"Connection successful.","testedAt":"..."}

Once configured, the worker automatically enriches completed assessment results with migration path recommendations, risk analysis, and remediation steps.

Customizing report generation

Report policy is stored in report-config/report-instructions.md. Docker Compose mounts this file at /app/config/report-instructions.md, and the API reloads it for every report. Edit the host file directly or use the administrator-only Report Generation Instructions editor on the AI Settings page. The editor displays the effective source, SHA-256 hash and 65,536-byte limit. Saves use the last-read hash to prevent silently overwriting a concurrent change. Changes affect new reports immediately; they do not require an image rebuild or application restart.

The same container path is used in Azure Container Apps, backed by the dedicated report-config Azure Files share. Each generated evidence bundle records the effective instruction content, source, and SHA-256 hash for auditability. The application keeps evidence-grounding requirements and the JSON response contract in protected code so custom guidance cannot remove those controls.

Configuration options:

ReportGeneration__InstructionsPath=/app/config/report-instructions.md
ReportGeneration__RequireExternalInstructions=false

When the external file is absent, the API initializes it from the packaged default when the mount is writable. If initialization is unavailable, it uses the packaged default unless RequireExternalInstructions is enabled. A read-only mount is supported for operator-managed policy, but the portal editor is then unavailable.


Application authentication

The portal uses application-managed users stored in PostgreSQL. Successful sign-in creates an HTTP-only, same-site authentication cookie, and mutating API requests require an antiforgery token. Routes under /api/agent/* continue to use PSK/HMAC authentication, while /health and the sign-in endpoints remain anonymous.

Set the initial administrator values in .env for Docker Compose:

APP_ADMIN_EMAIL=admin@example.com
APP_ADMIN_PASSWORD=<at-least-12-characters>
APP_ADMIN_DISPLAY_NAME=Administrator

The API provisions this account only when the application-user table is empty. Passwords are hashed with ASP.NET Core Identity and are never sent to the frontend after sign-in. Data Protection keys persist in a Docker volume locally and an Azure Files share in Azure so authentication cookies survive container restarts.

Azure deployment

Azure deployment uses Azure Container Apps via the Bicep template in infra/main.bicep. Configure and review the target environment explicitly; the repository does not contain private deployment account identifiers.

Copy-Item .\infra\main.parameters.json .\infra\main.parameters.local.json
# Edit the ignored local file with your environment and deployment metadata.
$env:AZURE_SUBSCRIPTION_ID = '<your-subscription-id>'
$env:AZURE_TENANT_ID = '<your-tenant-id>'

The deployment script prefers the ignored local parameter file when it exists. Use -ParametersFile to select a different file. For an existing deployment, preserve its original environmentName/resourceNameSuffix and storageAccountName; changing resource names can create new resources.

Parameters

Parameter Description
environmentName Environment tag value
resourceNameSuffix Resource naming suffix; defaults to environmentName
storageAccountName Optional storage name override; preserve it for existing deployments
location Azure region
sessionId, deployedBy, createdAt Deployment metadata tags
deployerObjectId Principal used by deployment role assignments
postgresAdminPassword Password for the PostgreSQL Flexible Server admin account (marked @secure)
appAdminEmail, appAdminDisplayName Initial application administrator identity
appAdminPassword Initial application administrator password (marked @secure)
workerServiceToken Worker/API service token (marked @secure)
configureSecrets Enables Key Vault secret creation; defaults to false
containerImage Common fallback image; defaults to a hello-world sample
apiContainerImage, workerContainerImage, frontendContainerImage, discoveryAgentContainerImage Exact deployable image references

Container Apps pull from ACR through the shared user-assigned managed identity; registry usernames and passwords are not deployment parameters. Portal authentication is application-managed in the current release.

Deploy steps

# 1. Build and validate the local application
.\build.ps1
docker compose build api worker frontend local-agent
docker compose up -d api worker frontend local-agent

# 2. Validate and preview Azure changes
.\infra\deploy.ps1 -Mode Validate
.\infra\deploy.ps1 -Mode WhatIf

# 3. Deploy only after reviewing the preview
.\infra\deploy.ps1 -Mode Deploy

Deployment does not rebuild application images. It resolves the immutable image ID used by each running Compose service, applies a timestamped ACR tag to that exact image, pushes it, and configures Azure Container Apps with those tags. Deployment stops if any required local service is not running.

Image-only application updates

For routine code releases to the existing Azure environment, build, publish, and activate all four images without redeploying infrastructure:

.\infra\deploy.ps1 -Mode ReleaseImages

This command builds and starts the local API, worker, frontend, and Discovery Agent containers; pushes each image to ACR with an immutable UTC timestamp tag and the latest alias; updates the existing Container Apps to the immutable tag; and verifies the API health and frontend endpoints. Container Apps remain pinned to immutable tags because pushing latest alone does not create a new revision. Use the regular deployment modes only for infrastructure or application-configuration changes.

Retrieve the URL from Azure rather than assuming the environment DNS suffix:

$fqdn = az containerapp show `
  --resource-group '<your-resource-group>' `
  --name '<your-frontend-container-app>' `
  --query properties.configuration.ingress.fqdn -o tsv
"https://$fqdn"

Configuration reference

API (src/backend/AssessmentToolkit.Api)

Key Description Default
ConnectionStrings__Postgres PostgreSQL connection string —
Api__PublicUrl Public base URL returned in bootstrap packages Request origin
Evidence__StorageRoot Directory for uploaded evidence files /app/evidence
DataProtection__KeysPath Durable ASP.NET Core key-ring directory used to protect portal-entered credentials Platform default
AiFoundry__Endpoint Azure OpenAI endpoint URL —
AiFoundry__Deployment Model deployment name (e.g. gpt-4o) —
AZURE_OPENAI_API_KEY Azure OpenAI API key; alternative to the encrypted portal credential —
ReportGeneration__InstructionsPath External Markdown report-policy path /app/config/report-instructions.md in supplied deployments
ReportGeneration__RequireExternalInstructions Fail report generation instead of using the packaged default when the external file is unavailable false

Worker (src/backend/AssessmentToolkit.Worker)

Key Description Default
ConnectionStrings__Postgres PostgreSQL connection string —
Worker__ApiBaseUrl Internal URL of the API service —

Agent (src/agent/AssessmentToolkit.Agent)

Variable Description
AGENT_ID Agent GUID (from bootstrap package)
AGENT_PSK Pre-shared key (from bootstrap package)
AGENT_BACKEND_URL Portal API base URL
AGENT_PLUGINS Comma-separated plugin IDs: postgresql, oracle, sqlserver, mysql, mongodb, cassandra, sybase
AGENT_BOOTSTRAP_PATH Path to bootstrap.json (alternative to env vars)
AGENT_DATABASES_PATH Path to local database credential profiles (default: databases.json)
AGENT_DB_n_ID Inventory database GUID for local profile index n, starting at zero
AGENT_DB_n_PLUGIN Plugin ID for local profile n
AGENT_DB_n_HOST, AGENT_DB_n_PORT, AGENT_DB_n_NAME Connection target for local profile n
AGENT_DB_n_USER, AGENT_DB_n_PASSWORD Read-only credentials for local profile n

Security notes

  • All agent-to-backend communication is authenticated with HMAC-SHA256 (method + path + timestamp + request nonce + body hash).
  • Timestamps are validated within a ±5 minute window, and each signed GUID nonce is accepted only once. Accepted nonces are stored in PostgreSQL so replay rejection is atomic across API replicas.
  • PSKs are stored as salted SHA-256 hashes in the database — the raw value is shown once at registration and never recoverable.
  • AI API keys entered in the portal are encrypted at rest with ASP.NET Core Data Protection, never returned by the API, and require a persistent protected key ring for recovery after restart.
  • Use TLS in production — the portal API should be served behind HTTPS; the agent verifies the server certificate.
  • The evidence storage volume should be backed up regularly; it holds uploaded AWR and Statspack files.

About

Self-hosted database migration assessment toolkit with a .NET API, React portal, Docker discovery agents, and AI-assisted reporting.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages