{
  "steward": "security-steward",
  "project": "Azure-AI-RAG-CSharp-Semantic-Kernel-Functions",
  "runDate": "2026-03-21",
  "runId": "2026-03-21T00-00-00",
  "findings": [
    {
      "id": "SEC-AUTH-001",
      "title": "No authentication configured on API",
      "severity": "critical",
      "category": "AUTH",
      "file": "src/ChatAPI/Program.cs",
      "line": 93,
      "description": "Program.cs calls app.UseAuthorization() but never registers any authentication scheme. AddAuthentication() is absent. UseAuthorization() without UseAuthentication() is a no-op. All endpoints are effectively anonymous.",
      "recommendation": "Add builder.Services.AddAuthentication().AddMicrosoftIdentityWebApi(...) and app.UseAuthentication() before app.UseAuthorization(). Integrate with Azure AD / Entra ID.",
      "status": "open"
    },
    {
      "id": "SEC-AUTH-002",
      "title": "All API endpoints publicly accessible without authorization",
      "severity": "critical",
      "category": "AUTH",
      "file": "src/ChatAPI/Controllers/ChatController.cs",
      "description": "Neither ChatController (POST /chat) nor SessionController (GET /session) carries an [Authorize] attribute. The App Service has publicNetworkAccess: Enabled. Both endpoints are reachable by any internet caller without authentication.",
      "recommendation": "Add [Authorize] to ChatController and SessionController. Define and enforce named authorization policies.",
      "status": "open"
    },
    {
      "id": "SEC-INJECT-001",
      "title": "Prompt injection: raw user input passed to AI model without sanitization",
      "severity": "critical",
      "category": "INJECT",
      "file": "src/ChatAPI/Services/ChatService.cs",
      "line": 40,
      "description": "The user-supplied question string is added to ChatHistory and submitted directly to GPT-4o via Semantic Kernel with no length limit, content filtering, or sanitization. A crafted input can override or bypass system prompt instructions (prompt injection).",
      "recommendation": "Enforce a maximum input length (e.g., 2000 characters). Integrate Azure AI Content Safety to pre-screen user inputs. Consider input allowlisting or pattern filtering for known injection patterns.",
      "status": "open"
    },
    {
      "id": "SEC-FRONTEND-001",
      "title": "Unsanitized AI HTML output rendered in browser via html-react-parser",
      "severity": "critical",
      "category": "FRONTEND",
      "file": "src/web/src/SupportAgent/ChatLayout.js",
      "line": 16,
      "description": "ChatLayout.js calls parse(obj.message) where obj.message is the raw HTML string returned by the AI model. html-react-parser does not sanitize HTML. The system prompt instructs the model to return HTML. A prompt injection attack could cause the model to return malicious HTML (script tags, event handler attributes) that is executed in the user's browser.",
      "recommendation": "Pass the HTML through DOMPurify.sanitize() before calling html-react-parser. Alternatively, adopt a markdown rendering approach (e.g., react-markdown) and instruct the model to return Markdown instead of raw HTML.",
      "status": "open"
    },
    {
      "id": "SEC-INFRA-003",
      "title": "CosmosDB key-based authentication not disabled",
      "severity": "critical",
      "category": "INFRA",
      "file": "infra/core/database/cosmos-db/account.bicep",
      "line": 44,
      "description": "The CosmosDB account is deployed with disableLocalAuth: false, leaving key-based authentication active. Anyone who obtains the primary or secondary key from the Azure Portal can access the database, bypassing the Managed Identity control plane entirely.",
      "recommendation": "Set disableLocalAuth: true in infra/core/database/cosmos-db/account.bicep and ensure the nosql/account.bicep wrapper passes disableKeyBasedAuth: true.",
      "status": "open"
    },
    {
      "id": "SEC-AUTH-003",
      "title": "No session ownership validation — any caller can access any session",
      "severity": "notable",
      "category": "AUTH",
      "file": "src/ChatAPI/Controllers/ChatController.cs",
      "line": 20,
      "description": "The POST /chat endpoint accepts a sessionId from the request body and loads the corresponding chat history from CosmosDB without verifying that the requesting user owns that session. Any caller who knows a valid session ID can read and append to another user's chat history.",
      "recommendation": "After adding authentication (SEC-AUTH-001), bind sessionId to the authenticated user's identity (e.g., store ownerId on ChatMessage) and validate ownership before loading or writing chat history.",
      "status": "open"
    },
    {
      "id": "SEC-CORS-001",
      "title": "CORS policy allows all origins, all methods, all headers",
      "severity": "notable",
      "category": "CORS",
      "file": "src/ChatAPI/Program.cs",
      "line": 71,
      "description": "The AllowAll CORS policy is configured with AllowAnyOrigin(), AllowAnyHeader(), and AllowAnyMethod(), and applied globally. Any website can make cross-origin requests to the API. This is unnecessarily permissive for a production service.",
      "recommendation": "Replace AllowAnyOrigin() with .WithOrigins() specifying only the React frontend's origin (e.g., the azurewebsites.net domain). Restrict allowed methods to POST and GET only.",
      "status": "open"
    },
    {
      "id": "SEC-HEADER-001",
      "title": "Swagger UI unconditionally enabled in all environments including production",
      "severity": "notable",
      "category": "HEADER",
      "file": "src/ChatAPI/Program.cs",
      "line": 85,
      "description": "The environment check around app.UseSwagger() and app.UseSwaggerUI() is commented out, meaning Swagger UI and the OpenAPI JSON descriptor are publicly accessible in production. This exposes the full API surface and schema to any attacker.",
      "recommendation": "Restore the environment guard: if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }",
      "status": "open"
    },
    {
      "id": "SEC-HEADER-002",
      "title": "Missing security headers: X-Content-Type-Options, X-Frame-Options, Content-Security-Policy",
      "severity": "notable",
      "category": "HEADER",
      "file": "src/ChatAPI/Program.cs",
      "description": "No security response headers are configured. Missing X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy, and Referrer-Policy.",
      "recommendation": "Add security headers middleware to Program.cs. Use a library such as NetEscapades.AspNetCore.SecurityHeaders or add a custom middleware to set the required headers on all responses. Also add app.UseHsts() for production HSTS enforcement.",
      "status": "open"
    },
    {
      "id": "SEC-INFRA-001",
      "title": "Storage account has public network access enabled and shared key access allowed",
      "severity": "notable",
      "category": "INFRA",
      "file": "infra/core/storage/blob-storage-account.bicep",
      "line": 14,
      "description": "The storage account is deployed with publicNetworkAccess: Enabled and allowSharedKeyAccess: true. Since Managed Identity is used exclusively, shared key access should be disabled. Public network access should be restricted to the function app's IP range or VNET, or use private endpoints.",
      "recommendation": "Set allowSharedKeyAccess: false. Consider setting publicNetworkAccess: Disabled and using private endpoints for the function app and API to access storage, or restrict with networkAcls.",
      "status": "open"
    },
    {
      "id": "SEC-INFRA-004",
      "title": "Key Vault not deployed; CosmosDB connection string set to empty in App Service config",
      "severity": "notable",
      "category": "INFRA",
      "file": "infra/core/security/key-vault.bicep",
      "description": "The key-vault.bicep file is essentially empty — no Key Vault resource is deployed. The API App Service has CosmosDb_ConnectionString set to an empty string, meaning this sensitive value must be manually entered after deployment (likely pasted in plaintext). The loader function has a KeyVaultUri app setting that is also empty, indicating incomplete integration.",
      "recommendation": "Define a Key Vault resource in key-vault.bicep with enableSoftDelete: true and enablePurgeProtection: true. Store the CosmosDB connection string as a Key Vault secret. Reference it via Key Vault reference syntax in the App Service appSettings (@Microsoft.KeyVault(SecretUri=...)).",
      "status": "open"
    },
    {
      "id": "SEC-DEP-001",
      "title": "Unpinned Python dependencies and pre-release NuGet packages in production",
      "severity": "notable",
      "category": "DEP",
      "file": "src/DocumentLoaderFunction/requirements.txt",
      "description": "Python requirements.txt has unpinned versions for langchain, langchain-openai, langchain-community, requests, azure-functions, and beautifulsoup4. LangChain has had multiple security advisories. The C# project uses pre-release packages Azure.AI.OpenAI 2.1.0-beta.2 and Azure.Search.Documents 11.6.0-beta.3 in production.",
      "recommendation": "Pin all Python dependencies to specific tested versions using pip freeze or pip-compile. Replace pre-release NuGet packages with their latest stable equivalents when available.",
      "status": "open"
    },
    {
      "id": "SEC-SECRET-001",
      "title": "Python function writes live bearer token to os.environ",
      "severity": "notable",
      "category": "SECRET",
      "file": "src/DocumentLoaderFunction/function_app.py",
      "line": 52,
      "description": "The function acquires a Cognitive Services bearer token and writes it to os.environ[OPENAI_API_KEY] and os.environ[AZURE_OPENAI_AD_TOKEN]. This places a live credential in the global process environment for the lifetime of the function process, where it could be logged or read by co-located code.",
      "recommendation": "Use the azure_ad_token_provider parameter of AzureOpenAIEmbeddings (a callable that returns a fresh token on demand) instead of writing the token to os.environ. This avoids persisting the token in global state.",
      "status": "open"
    },
    {
      "id": "SEC-INFRA-002",
      "title": "Overly permissive storage role assignments — three overlapping roles granted",
      "severity": "minor",
      "category": "INFRA",
      "file": "infra/core/storage/blob-storage-account.bicep",
      "line": 55,
      "description": "The managed identity is granted Storage Blob Data Contributor, Storage Blob Data Owner, and Storage Account Contributor simultaneously. Storage Blob Data Owner subsumes Contributor. Granting all three violates the principle of least privilege.",
      "recommendation": "Determine the minimum role required by each consuming service. The Azure Function needs Storage Blob Data Contributor and Storage Queue Data Contributor. Remove Storage Blob Data Owner and Storage Account Contributor unless there is a specific documented need.",
      "status": "open"
    },
    {
      "id": "SEC-INFRA-005",
      "title": "Blob containers load, completed, and images lack explicit publicAccess: None",
      "severity": "minor",
      "category": "INFRA",
      "file": "infra/core/storage/blob-storage-account.bicep",
      "line": 28,
      "description": "Three of the four blob containers (load, completed, images) do not have publicAccess: None set. Only the archive container explicitly sets this. Without allowBlobPublicAccess: false at the account level, these containers could be inadvertently set to public access.",
      "recommendation": "Add properties: { publicAccess: 'None' } to the load, completed, and images container resource definitions. Also set allowBlobPublicAccess: false at the storage account level.",
      "status": "open"
    },
    {
      "id": "SEC-FRONTEND-002",
      "title": "API response logged to browser console in production",
      "severity": "minor",
      "category": "FRONTEND",
      "file": "src/web/src/SupportAgent/Agent.js",
      "line": 38,
      "description": "console.log(res) logs the raw API response object to the browser console unconditionally. This is visible to any user with DevTools open and leaks response structure.",
      "recommendation": "Remove the console.log statement or guard it with if (process.env.NODE_ENV === 'development') { console.log(res); }",
      "status": "open"
    },
    {
      "id": "SEC-INFO-001",
      "title": "Managed Identity used consistently across all services",
      "severity": "info",
      "category": "INFO",
      "description": "DefaultAzureCredential is used in both the C# API and the Python function. All Azure service connections (OpenAI, AI Search, Storage) use Managed Identity rather than API keys. This is a strong authentication baseline.",
      "status": "open"
    },
    {
      "id": "SEC-INFO-002",
      "title": "CosmosDB queries use parameterized QueryDefinition — NoSQL injection mitigated",
      "severity": "info",
      "category": "INFO",
      "file": "src/ChatAPI/Data/ChatHistoryData.cs",
      "description": "All Cosmos DB queries in ChatHistoryData and ProductData use QueryDefinition with .WithParameter(), the correct parameterized query pattern. No string concatenation was found in any Cosmos query. NoSQL injection risk is mitigated.",
      "status": "open"
    }
  ],
  "summary": {
    "critical": 5,
    "notable": 7,
    "minor": 3,
    "info": 2,
    "total": 17
  }
}
