Skip to main content
Glama
advanced-security.test.ts26.6 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { AccessToken } from "@azure/identity"; import { describe, expect, it } from "@jest/globals"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebApi } from "azure-devops-node-api"; import { Alert, AlertType, AlertValidityStatus, Confidence, Severity, State } from "azure-devops-node-api/interfaces/AlertInterfaces"; import { PagedList } from "azure-devops-node-api/interfaces/common/VSSInterfaces"; import { configureAdvSecTools } from "../../../src/tools/advanced-security"; type TokenProviderMock = () => Promise<string>; type ConnectionProviderMock = () => Promise<WebApi>; interface AlertApiMock { getAlerts: jest.Mock; getAlert: jest.Mock; } describe("configureAdvSecTools", () => { let server: McpServer; let tokenProvider: TokenProviderMock; let connectionProvider: ConnectionProviderMock; let mockConnection: { getAlertApi: jest.Mock }; let mockAlertApi: AlertApiMock; beforeEach(() => { server = { tool: jest.fn() } as unknown as McpServer; tokenProvider = jest.fn(); mockAlertApi = { getAlerts: jest.fn(), getAlert: jest.fn(), }; mockConnection = { getAlertApi: jest.fn().mockResolvedValue(mockAlertApi), }; connectionProvider = jest.fn().mockResolvedValue(mockConnection); }); describe("tool registration", () => { it("registers Advanced Security tools on the server", () => { configureAdvSecTools(server, tokenProvider, connectionProvider); expect(server.tool as jest.Mock).toHaveBeenCalled(); }); }); describe("advsec_get_alerts tool", () => { it("should call getAlerts API with correct parameters and return multiple alerts", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const mockResult: PagedList<Alert> = [ { alertId: 1, state: State.Active, severity: Severity.High, alertType: AlertType.Code, title: "SQL Injection vulnerability", physicalLocations: [ { filePath: "src/database.js", region: { lineStart: 15, lineEnd: 17, }, }, ], }, { alertId: 2, state: State.Active, severity: Severity.Medium, alertType: AlertType.Code, title: "Cross-site scripting (XSS) vulnerability", physicalLocations: [ { filePath: "src/ui/form.js", region: { lineStart: 42, lineEnd: 45, }, }, ], }, { alertId: 3, state: State.Active, severity: Severity.Low, alertType: AlertType.Dependency, title: "Outdated dependency with known vulnerability", physicalLocations: [ { filePath: "package.json", region: { lineStart: 25, lineEnd: 25, }, }, ], }, ]; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(mockResult); const params = { project: "test-project", repository: "test-repo", alertType: "code", states: ["active"], severities: ["high"], }; const result = await handler(params); expect(mockAlertApi.getAlerts).toHaveBeenCalledWith( "test-project", "test-repo", undefined, // top undefined, // orderBy { alertType: AlertType.Code, states: [State.Active], severities: [Severity.High], }, undefined, // expand undefined // continuationToken ); expect(result.isError).toBeUndefined(); const returnedAlerts = JSON.parse(result.content[0].text); expect(result.content[0].text).toBe(JSON.stringify(returnedAlerts, null, 2)); expect(returnedAlerts).toHaveLength(3); expect(returnedAlerts[0].alertId).toBe(1); expect(returnedAlerts[0].title).toBe("SQL Injection vulnerability"); expect(returnedAlerts[1].alertId).toBe(2); expect(returnedAlerts[1].title).toBe("Cross-site scripting (XSS) vulnerability"); expect(returnedAlerts[2].alertId).toBe(3); expect(returnedAlerts[2].title).toBe("Outdated dependency with known vulnerability"); }); it("should handle pagination with continuation token", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; // First call - returns first page (simulating PagedList without continuation token in response) const firstPageMockResult: PagedList<Alert> = [ { alertId: 1, state: State.Active, severity: Severity.High, alertType: AlertType.Secret, title: "AWS access key in code", physicalLocations: [ { filePath: "src/aws-client.js", region: { lineStart: 5, lineEnd: 5, }, }, ], }, { alertId: 2, state: State.Active, severity: Severity.Medium, alertType: AlertType.Secret, title: "GitHub token exposed", physicalLocations: [ { filePath: "src/github-api.js", region: { lineStart: 12, lineEnd: 12, }, }, ], }, ]; firstPageMockResult.continuationToken = "next-page-token-abc123"; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValueOnce(firstPageMockResult); const firstParams = { project: "test-project", repository: "test-repo", alertType: "secret", states: ["active"], severities: ["medium", "high"], top: 2, }; const firstResult = await handler(firstParams); // Verify first call expect(mockAlertApi.getAlerts).toHaveBeenCalledWith( "test-project", "test-repo", 2, // top undefined, // orderBy { alertType: AlertType.Secret, states: [State.Active], severities: [Severity.Medium, Severity.High], }, undefined, // expand undefined // continuationToken ); // Second call const secondPageMockResult: PagedList<Alert> = [ { alertId: 3, state: State.Active, severity: Severity.High, alertType: AlertType.Secret, title: "Database password in plaintext", physicalLocations: [ { filePath: "src/database/connection.js", region: { lineStart: 8, lineEnd: 8, }, }, ], }, { alertId: 4, state: State.Active, severity: Severity.Medium, alertType: AlertType.Secret, title: "API key in configuration file", physicalLocations: [ { filePath: "config/production.json", region: { lineStart: 15, lineEnd: 15, }, }, ], }, ]; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValueOnce(secondPageMockResult); const secondParams = { project: "test-project", repository: "test-repo", alertType: "secret", states: ["active"], severities: ["medium", "high"], top: 2, continuationToken: "next-page-token-abc123", }; const secondResult = await handler(secondParams); // Verify second call with continuation token expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", 2, // top undefined, // orderBy { alertType: AlertType.Secret, states: [State.Active], severities: [Severity.Medium, Severity.High], }, undefined, // expand "next-page-token-abc123" // continuationToken ); // Verify both results expect(firstResult.isError).toBeUndefined(); expect(secondResult.isError).toBeUndefined(); const firstPageAlerts = JSON.parse(firstResult.content[0].text); const secondPageAlerts = JSON.parse(secondResult.content[0].text); // Verify we get different alerts for each page expect(firstPageAlerts).toHaveLength(2); expect(firstPageAlerts[0].alertId).toBe(1); expect(firstPageAlerts[1].alertId).toBe(2); expect(secondPageAlerts).toHaveLength(2); expect(secondPageAlerts[0].alertId).toBe(3); expect(secondPageAlerts[1].alertId).toBe(4); // Verify both pages have secret alert types expect(firstPageAlerts[0].alertType).toBe(AlertType.Secret); expect(firstPageAlerts[1].alertType).toBe(AlertType.Secret); expect(secondPageAlerts[0].alertType).toBe(AlertType.Secret); expect(secondPageAlerts[1].alertType).toBe(AlertType.Secret); }); it("should handle API errors gracefully", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const testError = new Error("Failed to retrieve alerts"); (mockAlertApi.getAlerts as jest.Mock).mockRejectedValue(testError); const params = { project: "test-project", repository: "test-repo", }; const result = await handler(params); expect(mockAlertApi.getAlerts).toHaveBeenCalled(); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error fetching Advanced Security alerts: Failed to retrieve alerts"); }); it("should handle null API results correctly", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(null); const params = { project: "test-project", repository: "test-repo", }; const result = await handler(params); expect(mockAlertApi.getAlerts).toHaveBeenCalled(); expect(result.content[0].text).toBe("null"); }); it("should conditionally include confidenceLevels and validity only for secret alerts", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const mockResult: PagedList<Alert> = [ { alertId: 123, alertType: AlertType.Secret, state: State.Active, severity: Severity.High, }, ]; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(mockResult); // Test 1: Secret alert type - should include confidenceLevels and validity const secretParams = { project: "test-project", repository: "test-repo", alertType: "secret", confidenceLevels: ["high"], validity: ["active"], }; await handler(secretParams); expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", undefined, // top undefined, // orderBy expect.objectContaining({ alertType: AlertType.Secret, confidenceLevels: [Confidence.High], validity: [AlertValidityStatus.Active], }), undefined, // expand undefined // continuationToken ); // Test 2: Code alert type - should NOT include confidenceLevels and validity const codeParams = { project: "test-project", repository: "test-repo", alertType: "code", confidenceLevels: ["high"], validity: ["active"], }; await handler(codeParams); expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", undefined, // top undefined, // orderBy expect.objectContaining({ alertType: AlertType.Code, }), undefined, // expand undefined // continuationToken ); // Verify that confidenceLevels and validity are NOT in the criteria for code alerts const lastCall = (mockAlertApi.getAlerts as jest.Mock).mock.calls[1]; const criteriaForCodeAlert = lastCall[4]; expect(criteriaForCodeAlert).not.toHaveProperty("confidenceLevels"); expect(criteriaForCodeAlert).not.toHaveProperty("validity"); // Test 3: No alert type specified - should include confidenceLevels and validity (allowing all types including secrets) const noTypeParams = { project: "test-project", repository: "test-repo", confidenceLevels: ["high"], validity: ["active"], }; await handler(noTypeParams); expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", undefined, // top undefined, // orderBy expect.objectContaining({ confidenceLevels: [Confidence.High], validity: [AlertValidityStatus.Active], }), undefined, // expand undefined // continuationToken ); }); it("should handle optional parameters correctly when not provided", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const mockResult: PagedList<Alert> = []; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(mockResult); // Test with minimal parameters - only required ones const minimalParams = { project: "test-project", repository: "test-repo", }; await handler(minimalParams); // When optional parameters aren't provided, they remain undefined expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", undefined, // top (optional, no default applied by handler) undefined, // orderBy (optional, no default applied by handler) {}, // empty criteria object since no optional filters provided undefined, // expand undefined // continuationToken ); }); it("should include all optional parameters when provided", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const mockResult: PagedList<Alert> = []; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(mockResult); // Test with all optional parameters provided const allParamsParams = { project: "test-project", repository: "test-repo", alertType: "dependency", states: ["active", "dismissed"], severities: ["high", "medium"], ruleId: "rule123", ruleName: "security-rule", toolName: "CodeQL", ref: "refs/heads/main", onlyDefaultBranch: false, top: 50, orderBy: "id", continuationToken: "token123", }; await handler(allParamsParams); expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", 50, // top "id", // orderBy expect.objectContaining({ alertType: AlertType.Dependency, states: [State.Active, State.Dismissed], severities: [Severity.High, Severity.Medium], ruleId: "rule123", ruleName: "security-rule", toolName: "CodeQL", ref: "refs/heads/main", onlyDefaultBranch: false, }), undefined, // expand "token123" // continuationToken ); // Verify all optional fields are included const lastCall = (mockAlertApi.getAlerts as jest.Mock).mock.calls[0]; const criteria = lastCall[4]; expect(criteria).toHaveProperty("alertType", AlertType.Dependency); expect(criteria).toHaveProperty("states", [State.Active, State.Dismissed]); expect(criteria).toHaveProperty("severities", [Severity.High, Severity.Medium]); expect(criteria).toHaveProperty("ruleId", "rule123"); expect(criteria).toHaveProperty("ruleName", "security-rule"); expect(criteria).toHaveProperty("toolName", "CodeQL"); expect(criteria).toHaveProperty("ref", "refs/heads/main"); expect(criteria).toHaveProperty("onlyDefaultBranch", false); }); it("should handle onlyDefaultBranch parameter correctly", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const mockResult: PagedList<Alert> = []; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(mockResult); // Test with onlyDefaultBranch explicitly set to false const falseParams = { project: "test-project", repository: "test-repo", onlyDefaultBranch: false, }; await handler(falseParams); expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", undefined, // top (not provided) undefined, // orderBy (not provided) expect.objectContaining({ onlyDefaultBranch: false, }), undefined, // expand undefined // continuationToken ); // Test with onlyDefaultBranch explicitly set to true const trueParams = { project: "test-project", repository: "test-repo", onlyDefaultBranch: true, }; await handler(trueParams); expect(mockAlertApi.getAlerts).toHaveBeenLastCalledWith( "test-project", "test-repo", undefined, // top (not provided) undefined, // orderBy (not provided) expect.objectContaining({ onlyDefaultBranch: true, }), undefined, // expand undefined // continuationToken ); }); it("should handle secret alerts without confidenceLevels or validity", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; const mockResult: PagedList<Alert> = []; (mockAlertApi.getAlerts as jest.Mock).mockResolvedValue(mockResult); // Test secret alert without confidenceLevels or validity explicitly provided const secretWithoutParams = { project: "test-project", repository: "test-repo", alertType: "secret", }; await handler(secretWithoutParams); const lastCall = (mockAlertApi.getAlerts as jest.Mock).mock.calls[0]; const criteria = lastCall[4]; expect(criteria).toHaveProperty("alertType", AlertType.Secret); // confidenceLevels and validity should not be included if not explicitly provided expect(criteria).not.toHaveProperty("confidenceLevels"); expect(criteria).not.toHaveProperty("validity"); }); it("should handle non-Error exception types", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alerts"); if (!call) throw new Error("advsec_get_alerts tool not registered"); const [, , , handler] = call; // Test with non-Error exception (string) (mockAlertApi.getAlerts as jest.Mock).mockRejectedValue("String error"); const params = { project: "test-project", repository: "test-repo", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error fetching Advanced Security alerts: Unknown error occurred"); }); }); describe("advsec_get_alert_details tool", () => { it("should fetch specific alert details", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alert_details"); if (!call) throw new Error("advsec_get_alert_details tool not registered"); const [, , , handler] = call; const mockResult: Alert = { alertId: 1, state: State.Active, severity: Severity.High, alertType: AlertType.Code, title: "Test security alert", physicalLocations: [ { filePath: "src/test.js", region: { lineStart: 10, lineEnd: 12, }, }, ], }; (mockAlertApi.getAlert as jest.Mock).mockResolvedValue(mockResult); const params = { project: "test-project", repository: "test-repo", alertId: 1, }; const result = await handler(params); expect(mockAlertApi.getAlert).toHaveBeenCalledWith( "test-project", 1, "test-repo", undefined, // ref undefined // expand ); expect(result.content[0].text).toBe(JSON.stringify(mockResult, null, 2)); expect(result.isError).toBeUndefined(); }); it("should fetch specific alert details with ref parameter", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alert_details"); if (!call) throw new Error("advsec_get_alert_details tool not registered"); const [, , , handler] = call; const mockResult: Alert = { alertId: 1, state: State.Active, severity: Severity.High, alertType: AlertType.Code, title: "Test security alert", }; (mockAlertApi.getAlert as jest.Mock).mockResolvedValue(mockResult); const params = { project: "test-project", repository: "test-repo", alertId: 1, ref: "refs/heads/feature-branch", }; const result = await handler(params); expect(mockAlertApi.getAlert).toHaveBeenCalledWith( "test-project", 1, "test-repo", "refs/heads/feature-branch", // ref undefined // expand ); expect(result.content[0].text).toBe(JSON.stringify(mockResult, null, 2)); expect(result.isError).toBeUndefined(); }); it("should handle API errors correctly", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alert_details"); if (!call) throw new Error("advsec_get_alert_details tool not registered"); const [, , , handler] = call; const testError = new Error("Alert not found"); (mockAlertApi.getAlert as jest.Mock).mockRejectedValue(testError); const params = { project: "test-project", repository: "test-repo", alertId: 999, }; const result = await handler(params); expect(mockAlertApi.getAlert).toHaveBeenCalled(); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error fetching alert details: Alert not found"); }); it("should handle non-Error exception types", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alert_details"); if (!call) throw new Error("advsec_get_alert_details tool not registered"); const [, , , handler] = call; // Test with non-Error exception (string) (mockAlertApi.getAlert as jest.Mock).mockRejectedValue("String error"); const params = { project: "test-project", repository: "test-repo", alertId: 999, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error fetching alert details: Unknown error occurred"); }); it("should handle null API results correctly", async () => { configureAdvSecTools(server, tokenProvider, connectionProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "advsec_get_alert_details"); if (!call) throw new Error("advsec_get_alert_details tool not registered"); const [, , , handler] = call; (mockAlertApi.getAlert as jest.Mock).mockResolvedValue(null); const params = { project: "test-project", repository: "test-repo", alertId: 1, }; const result = await handler(params); expect(mockAlertApi.getAlert).toHaveBeenCalled(); expect(result.content[0].text).toBe("null"); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/magemaclean/azure-devops-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server