Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,6 @@ project.nuget.cache
*.prompt.local.md
.github/copilot-cache/
.github/README.md
How-To-Use.md
How-To-Use.md

docs/linkedin
53 changes: 50 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ Real-time runtime metrics for the ServiceHub API itself: uptime, memory usage, t

---

## 🚦 Recommended Usage Flow

Follow this path before connecting to a production namespace. This protects your live environment and gives you confidence in every operation before it matters.

| Step | Environment | Goal |
|------|-------------|------|
| **Step 1** | **DEV** | Connect your development Service Bus namespace. Explore message browsing, DLQ inspection, AI pattern analysis, and auto-replay rules in a safe environment where mistakes are harmless. |
| **Step 2** | **UAT** | Repeat in your UAT namespace with realistic production-like data. Validate replay targets, confirm rule logic, review AI findings, and check that scheduled messages behave as expected. |
| **Step 3** | **PROD** | Connect only after DEV and UAT validation. Production namespaces enforce read-only browsing by default — Quick Actions (replay, send, generate) are disabled to prevent accidental data modification. |

> ⚠️ **Do NOT connect a production Service Bus namespace without prior validation in DEV and UAT.**
> While ServiceHub is read-only by default, replay and send operations are destructive.
> Validate your replay rules and message targets in lower environments first.

---

## ⚡ Quick Start

### One-Command Setup (Recommended)
Expand Down Expand Up @@ -356,14 +372,45 @@ For deep-dive architecture details, see [services/api/ARCHITECTURE.md](services/

---

## 🛡️ Security & Safety
## 🛡️ Security & Privacy

### What ServiceHub guarantees

- **Read-only by default** — Uses `PeekMessagesAsync`; messages are **never removed or consumed**
- **Minimal permissions** — Full functionality with Listen-only access
- **AES-GCM encryption** — Connection strings encrypted at rest; key stored in local config
- **AES-GCM encryption** — Connection strings encrypted at rest; key stored in local config, never returned to the browser
- **Zero external calls** — AI analysis runs entirely in-browser; no message data leaves your environment
- **No message persistence** — Messages displayed in-memory only during your session
- **No message persistence** — Messages are displayed in-memory only during your session; never written to a database
- **Production-safe** — Won't interfere with your active message consumers
- **Log redaction** — Backend logging pipeline strips connection strings, API keys, and access tokens before any log output

### What ServiceHub does NOT collect or store

| Data | Stored? | Notes |
|------|---------|-------|
| Connection strings | ❌ Never in plaintext | AES-GCM encrypted at rest; decrypted in memory only during use |
| Message bodies | ❌ Never | Displayed in-browser session memory only; not logged or persisted |
| User data / PII | ❌ Never | No user database exists |
| Message correlation IDs (business) | ❌ Never logged | Infrastructure correlation IDs for request tracing only |
| Customer / tenant data | ❌ Never | Messages never leave your own infrastructure |

### Application Insights telemetry (privacy-safe)

ServiceHub optionally emits telemetry to Azure Application Insights. When enabled, telemetry is strictly limited to:

- **Request durations** and HTTP status codes (not request/response bodies)
- **Error codes** and exception types (not exception messages containing secrets)
- **System-level metrics** — memory, GC, thread counts

The following is **explicitly excluded** from telemetry:

- Connection strings (redacted by `SensitiveDataTelemetryProcessor` and `LogRedactor`)
- Message bodies and payloads (message-body API endpoints excluded from auto-tracking)
- Business-level correlation IDs from message content
- User input fields
- API keys and tokens (redacted from query strings and headers)

Application Insights is **disabled by default** — it only activates when `ApplicationInsights:ConnectionString` is configured in `appsettings.Local.json`.

---

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/messages/MessageDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ function ActionButtons({ message, namespaceId }: ActionButtonsProps) {
setConfirmState({
isOpen: true,
title: 'Replay Message',
message: `Are you sure you want to replay message ${shortId}?\n\nThis will re-send the message to the queue for processing.\n\n⚠️ Replay is best-effort and not atomic. If a transient error occurs after the message is sent but before it is removed from the DLQ, both the original DLQ entry and the new copy may briefly coexist. Check ApplicationProperties for "Replayed=true" if you see unexpected duplicates.`,
message: `Are you sure you want to replay message ${shortId}?\n\nThis will re-send the message to the queue for processing.\n\n💡 Best practice: validate replay in DEV or UAT before using in PROD. Production namespaces block this action.\n\n⚠️ Replay is best-effort and not atomic. If a transient error occurs after the message is sent but before it is removed from the DLQ, both the original DLQ entry and the new copy may briefly coexist. Check ApplicationProperties for "Replayed=true" if you see unexpected duplicates.`,
variant: 'default',
action: 'replay',
});
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/lib/helpContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ export const tooltips = {
'Use the tabs to view Properties (metadata), Body (payload), AI Analysis (pattern detection), System Info, and Actions (replay / complete).',
} as TooltipContent,
replay: {
text: 'Resubmit this dead-letter message',
text: 'Resubmit this dead-letter message — use in DEV/UAT before PROD',
detail:
'Sends the message back to the original queue for reprocessing. Only available in Dev/UAT environments. Production namespaces block this action.',
action: 'Fix the root cause first, then click Replay.',
'Sends the message back to the original queue for reprocessing. Recommended: validate replay behaviour in DEV or UAT before applying to production traffic. Production namespaces block this action for safety.',
action: 'Fix the root cause first, validate in DEV/UAT, then click Replay.',
} as TooltipContent,
search: {
text: 'Filter messages by content',
Expand Down
27 changes: 18 additions & 9 deletions apps/web/src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,31 @@ const appInsights = new ApplicationInsights({

// Cost-effective settings
samplingPercentage: Number(import.meta.env.VITE_APPINSIGHTS_SAMPLING_PERCENTAGE) || 50,
disableFetchTracking: false,
enableCorsCorrelation: true,
enableAutoRouteTracking: true,

// Reduce telemetry volume
maxBatchInterval: 15000, // Batch every 15s instead of default 5s
maxBatchSizeInBytes: 102400, // 100KB batch size
disableExceptionTracking: false, // Keep exception tracking (critical)
disableAjaxTracking: false, // Keep API call tracking

// Exclude health check and noisy internal endpoints from AJAX tracking
correlationHeaderExcludedDomains: [],
// ── Privacy & security ────────────────────────────────────────────
// Never capture request or response bodies — message content must not be sent to telemetry
disableAjaxTracking: false, // Track API call durations and status codes (not bodies)
disableFetchTracking: false, // Track fetch durations and status codes (not bodies)
// Do NOT log any user input or identifiers to telemetry
disableCookiesUsage: true,
// Do NOT add correlation headers to cross-origin Service Bus requests
correlationHeaderExcludedDomains: ['*.servicebus.windows.net', '*.azure.com'],
// Exclude endpoints that carry message body data from AJAX auto-tracking
excludeRequestFromAutoTrackingPatterns: [
/\/api\/v1\/namespaces\/[^/]+\/queues\/[^/]+\/messages/i,
/\/api\/v1\/namespaces\/[^/]+\/topics\//i,
/\/api\/v1\/correlation/i,
/\/health/i,
/\/internal\//i,
],
// ─────────────────────────────────────────────────────────────────

// Reduce telemetry volume
maxBatchInterval: 15000, // Batch every 15s instead of default 5s
maxBatchSizeInBytes: 102400, // 100KB batch size
disableExceptionTracking: false, // Keep exception tracking (critical for error monitoring)
},
});

Expand Down
19 changes: 18 additions & 1 deletion apps/web/src/pages/ConnectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,24 @@ export function ConnectPage() {
<option value="uat">UAT — User Acceptance Testing</option>
<option value="prod">PROD — Production</option>
</select>
<p className="text-xs text-gray-500 mt-1">Production disables Quick Actions for safety.</p>
<p className="text-xs text-gray-500 mt-1">
{environment === 'prod' ? (
<>
<span className="text-amber-600 font-semibold">⚠️ Production namespace:</span>
{' '}Quick Actions (replay, send, generate) are disabled for safety. Validate your workflow in DEV and UAT first.
</>
) : environment === 'uat' ? (
<>
<span className="text-amber-700 font-medium">UAT namespace:</span>
{' '}Validate replay rules and DLQ behaviour here before connecting to PROD.
</>
) : (
<>
<span className="text-green-700 font-medium">Recommended: start with a DEV namespace.</span>
{' '}Test DLQ inspection, replay rules, and message operations safely before moving to UAT or PROD.
</>
)}
</p>
</div>

<button
Expand Down
78 changes: 78 additions & 0 deletions apps/web/src/pages/WelcomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,84 @@ export function WelcomePage() {
</div>
</section>

{/* ─── Safe Adoption Path ──────────────────────────────────────────── */}
<section className="px-6 pb-10">
<div className="max-w-4xl mx-auto">
<div className="rounded-xl border border-amber-200 bg-amber-50 p-5">
<div className="flex items-start gap-3 mb-4">
<div className="flex-shrink-0 w-9 h-9 bg-amber-100 rounded-lg flex items-center justify-center">
<Shield className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-sm font-bold text-amber-900">
Recommended Adoption Path
</p>
<p className="text-xs text-amber-700 mt-0.5">
Follow this flow before connecting to a production namespace.
</p>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4">
{[
{
step: '1',
env: 'DEV',
color: 'green',
title: 'Start in DEV',
desc: 'Connect your development namespace. Explore messages, test DLQ inspection, try auto-replay rules, and verify your workflow in a safe environment.',
},
{
step: '2',
env: 'UAT',
color: 'amber',
title: 'Validate in UAT',
desc: 'Repeat in your UAT namespace. Confirm replay behaviour with realistic data, validate auto-replay rules, and review AI findings against production-like traffic.',
},
{
step: '3',
env: 'PROD',
color: 'red',
title: 'Use PROD with confidence',
desc: 'Only after DEV and UAT validation. Production namespaces enforce read-only mode by default — Quick Actions are disabled to prevent accidental modifications.',
},
].map(({ step, env, color, title, desc }) => (
<div
key={env}
className={`flex flex-col gap-2 rounded-lg border p-3.5 bg-white ${
color === 'green' ? 'border-green-200' :
color === 'amber' ? 'border-amber-200' :
'border-red-200'
}`}
>
<div className="flex items-center gap-2">
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded uppercase tracking-wider ${
color === 'green' ? 'bg-green-100 text-green-700' :
color === 'amber' ? 'bg-amber-100 text-amber-700' :
'bg-red-100 text-red-700'
}`}>
{env}
</span>
<span className="text-xs font-semibold text-gray-600">Step {step}</span>
</div>
<p className="text-xs font-bold text-gray-900">{title}</p>
<p className="text-xs text-gray-600 leading-relaxed">{desc}</p>
</div>
))}
</div>

<div className="flex items-start gap-2 rounded-lg bg-red-50 border border-red-200 px-3 py-2.5">
<span className="text-red-500 text-sm mt-0.5 flex-shrink-0">⚠️</span>
<p className="text-xs text-red-800 font-medium">
<strong>Do not connect a production Service Bus namespace without prior validation in DEV and UAT.</strong>
{' '}While ServiceHub is read-only by default, replay and send operations are destructive.
Validate rule logic and replay targets in lower environments first.
</p>
</div>
</div>
</div>
</section>

{/* ─── Stats Bar ───────────────────────────────────────────────────── */}
<section className="py-12 px-6 bg-gradient-to-r from-primary-600 to-blue-600">
<div className="max-w-5xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-6 text-center text-white">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ public sealed class SensitiveDataTelemetryProcessor : ITelemetryProcessor
"api_key",
};

// Custom property keys that must never reach Application Insights
// regardless of how they are set (e.g. by an AI analyzer or controller).
private static readonly HashSet<string> SensitivePropertyKeys = new(StringComparer.OrdinalIgnoreCase)
{
"messageBody",
"message_body",
"body",
"connectionString",
"connection_string",
"correlationId", // Service Bus message-level correlation IDs (not infra tracing IDs)
"userInput",
"payload",
Comment on lines +43 to +47
};

private static readonly string[] SensitiveDependencyPatterns =
[
"SharedAccessKey",
Expand All @@ -55,6 +69,7 @@ public void Process(ITelemetry item)

case TraceTelemetry trace:
trace.Message = LogRedactor.Redact(trace.Message);
RedactProperties(trace.Properties);
break;

case ExceptionTelemetry exception:
Expand All @@ -64,12 +79,37 @@ public void Process(ITelemetry item)
case DependencyTelemetry dependency:
RedactDependencyTelemetry(dependency);
break;

case EventTelemetry evt:
// Custom events must never carry message bodies or connection data
RedactProperties(evt.Properties);
break;
}

// Always pass the item through — never drop telemetry in this processor
_next.Process(item);
}

/// <summary>
/// Removes or redacts known-sensitive property keys from a telemetry property bag.
/// Runs LogRedactor over all remaining values to catch any accidental leakage.
/// </summary>
private static void RedactProperties(IDictionary<string, string> properties)
Comment on lines +93 to +97
{
// Remove keys that should never appear in telemetry
foreach (var key in properties.Keys.ToList())
{
if (SensitivePropertyKeys.Contains(key))
{
properties.Remove(key);
}
else
{
properties[key] = LogRedactor.Redact(properties[key]);
}
}
}

private static void RedactRequestTelemetry(RequestTelemetry request)
{
var url = request.Url;
Expand Down Expand Up @@ -112,11 +152,8 @@ private static void RedactExceptionTelemetry(ExceptionTelemetry exception)
// Redact the outer message stored in telemetry properties
exception.Message = LogRedactor.Redact(exception.Message);

// Redact any custom properties that may contain secrets
foreach (var key in exception.Properties.Keys.ToList())
{
exception.Properties[key] = LogRedactor.Redact(exception.Properties[key]);
}
// Redact / remove sensitive custom properties
RedactProperties(exception.Properties);
Comment on lines 152 to +156
}

private static void RedactDependencyTelemetry(DependencyTelemetry dependency)
Expand Down
Loading