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.
- DB Migration Assessment Toolkit
- Table of contents
- Architecture
- Fictional customer example
- Prerequisites
- Quick start — local
- Portal walkthrough
- Offline database assessment
- Questionnaire templates
- Deleting projects
- Deploying Discovery Agents
- Discovery Agent user instructions
- Testing
- Database account requirements
- Enabling AI features
- Microsoft Entra authentication
- Azure deployment
- Configuration reference
- Security notes
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.
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.
| 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.ps1on a machine with the SDKs before runningdocker compose build.
# 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:3000The 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.
| 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 |
Use this workflow when a Discovery Agent cannot reach a database:
- Create the database definition in DB Inventory.
- In that database row, select Download offline assessment script and transfer the generated PowerShell script to the customer.
- 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>- 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.
- 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.
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"]
}allowCustomAnsweradds an Other / custom answer field and is valid only forSelectandMultiSelectquestions.allowAttachmentslets 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.
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.
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.
| 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.
- Navigate to Discovery Agents and select a project.
- Click + Register Discovery Agent.
- 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/orsybase - PSK Mode —
System generated(recommended) orUser supplied
- Click Register Discovery Agent.
- 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.
| 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 |
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.AgentFor 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.0Pull 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:latestEnvironment 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.
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 -dUse 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.
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:
- Edit
docker-compose.ymland fill inAGENT_IDandAGENT_PSKfrom 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"- Start with the
agentprofile:
docker compose --profile agent up -dNote:
AGENT_BACKEND_URLuses the internal Docker network nameapiwhen running alongside the portal stack.
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:latestThe agent looks for bootstrap.json in /app/ by default, or at the path set in AGENT_BOOTSTRAP_PATH.
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.dllFor 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.
Complete these steps after registering and deploying a Discovery Agent.
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.
- In DB Inventory, identify the database to assess. Retrieve its
databaseIdfromGET /api/databases?projectId=<project-guid>; for a local portal, usehttp://localhost:5000/api/databases?projectId=<project-guid>. - 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"
}
]
}- 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:latestThe 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.
docker start dbmr-agent
docker logs -f dbmr-agentA 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.
- Open Discovery Agents and select the project.
- Confirm the Discovery Agent has a recent heartbeat and the required plugin is listed.
- Select Dispatch Job.
- Choose a database from inventory to use its matching local credential profile, or enter the host, port, database/service name, username, and password manually.
- Confirm the plugin matches the database engine, then select Dispatch.
- 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.
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.
| 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. |
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-LocationThe 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.
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.ps1The temporary agent container is removed automatically. Test records use unique e2e-* names so repeated runs do not conflict.
After building the current Worker image and starting a disposable Compose stack with API migrations complete, run:
docker compose build worker
.\tests\WorkerPostgresSmoke.ps1This 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.
The agent uses a read-only dedicated account. Never use sa, SYS, or a DBA account.
-- 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.
-- 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.
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.
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.
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.
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.
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.
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.
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>"- Go to AI Settings (
/ai-settings). - Enter the Azure OpenAI endpoint, deployment name, and API key under Active Connection.
- Select Save Configuration. The key is encrypted with ASP.NET Core Data Protection before it is stored and is never returned to the browser.
- 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.
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.
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.
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=AdministratorThe 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 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.
| 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.
# 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 DeployDeployment 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.
For routine code releases to the existing Azure environment, build, publish, and activate all four images without redeploying infrastructure:
.\infra\deploy.ps1 -Mode ReleaseImagesThis 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"| 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 |
| Key | Description | Default |
|---|---|---|
ConnectionStrings__Postgres |
PostgreSQL connection string | — |
Worker__ApiBaseUrl |
Internal URL of the API service | — |
| 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 |
- 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.