diff --git a/hooks/persistence-elastic/hook/hook.js b/hooks/persistence-elastic/hook/hook.js index e9ea8077e..ebe682b4c 100644 --- a/hooks/persistence-elastic/hook/hook.js +++ b/hooks/persistence-elastic/hook/hook.js @@ -105,7 +105,10 @@ export async function handle({ }, ]); - const { body: bulkResponse } = await client.bulk({ refresh: true, body }); + const response = await client.bulk({ refresh: true, body }); + + // Support both ES 7.x (response.body) and ES 8.x (response directly) + const bulkResponse = response.body || response; if (bulkResponse.errors) { console.error("Bulk Request had errors:"); diff --git a/hooks/persistence-elastic/hook/hook.test.js b/hooks/persistence-elastic/hook/hook.test.js index 08b5e7b16..2b765a45a 100644 --- a/hooks/persistence-elastic/hook/hook.test.js +++ b/hooks/persistence-elastic/hook/hook.test.js @@ -218,3 +218,225 @@ test("should append week format like yyyy/'W'W -> 2020/W46", async () => { index: `scb_2020/W46`, }); }); + +test("should handle Elasticsearch 8.x response format (without body wrapper)", async () => { + const findings = [ + { + id: "4560b3e6-1219-4f5f-9b44-6579f5a32407", + name: "Port 5601 is open", + category: "Open Port", + }, + ]; + + const getFindings = async () => findings; + + // ES 8.x returns response directly without .body wrapper + const es8Client = { + indices: { + create: jest.fn(), + }, + index: jest.fn(), + bulk: jest.fn(() => ({ errors: false, items: [] })), // ES 8.x format + }; + + await handle({ + getFindings, + scan, + now: testDate, + tenant: "default", + appendNamespace: true, + client: es8Client, + }); + + expect(es8Client.bulk).toHaveBeenCalledTimes(1); + expect(es8Client.bulk).toHaveBeenCalledWith({ + refresh: true, + body: [ + { + index: { + _index: `scb_default_2020-11-11`, + }, + }, + { + "@timestamp": testDate, + category: "Open Port", + id: "4560b3e6-1219-4f5f-9b44-6579f5a32407", + name: "Port 5601 is open", + scan_id: "09988cdf-1fc7-4f85-95ee-1b1d65dbc7cc", + scan_labels: { + company: "iteratec", + }, + scan_name: "demo-scan", + scan_type: "Nmap", + type: "finding", + }, + ], + }); +}); + +test("should handle Elasticsearch 7.x response format (with body wrapper)", async () => { + const findings = [ + { + id: "test-finding-id", + name: "Test Finding", + category: "Test Category", + }, + ]; + + const getFindings = async () => findings; + + // ES 7.x returns response with .body wrapper + const es7Client = { + indices: { + create: jest.fn(), + }, + index: jest.fn(), + bulk: jest.fn(() => ({ body: { errors: false, items: [] } })), // ES 7.x format + }; + + await handle({ + getFindings, + scan, + now: testDate, + tenant: "default", + appendNamespace: true, + client: es7Client, + }); + + expect(es7Client.bulk).toHaveBeenCalledTimes(1); + expect(es7Client.bulk).toHaveBeenCalledWith({ + refresh: true, + body: [ + { + index: { + _index: `scb_default_2020-11-11`, + }, + }, + { + "@timestamp": testDate, + category: "Test Category", + id: "test-finding-id", + name: "Test Finding", + scan_id: "09988cdf-1fc7-4f85-95ee-1b1d65dbc7cc", + scan_labels: { + company: "iteratec", + }, + scan_name: "demo-scan", + scan_type: "Nmap", + type: "finding", + }, + ], + }); +}); + +test("should handle bulk errors in Elasticsearch 8.x format", async () => { + const findings = [ + { + id: "error-finding", + name: "Error Finding", + category: "Error Category", + }, + ]; + + const getFindings = async () => findings; + + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); + + // ES 8.x with errors + const es8ClientWithErrors = { + indices: { + create: jest.fn(), + }, + index: jest.fn(), + bulk: jest.fn(() => ({ + errors: true, + items: [ + { + index: { + error: { + type: "mapper_parsing_exception", + reason: "failed to parse field", + }, + }, + }, + ], + })), + }; + + await handle({ + getFindings, + scan, + now: testDate, + tenant: "default", + client: es8ClientWithErrors, + }); + + expect(consoleErrorSpy).toHaveBeenCalledWith("Bulk Request had errors:"); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.objectContaining({ + errors: true, + items: expect.any(Array), + }), + ); + + consoleErrorSpy.mockRestore(); + consoleLogSpy.mockRestore(); +}); + +test("should handle bulk errors in Elasticsearch 7.x format", async () => { + const findings = [ + { + id: "error-finding", + name: "Error Finding", + category: "Error Category", + }, + ]; + + const getFindings = async () => findings; + + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); + + // ES 7.x with errors + const es7ClientWithErrors = { + indices: { + create: jest.fn(), + }, + index: jest.fn(), + bulk: jest.fn(() => ({ + body: { + errors: true, + items: [ + { + index: { + error: { + type: "mapper_parsing_exception", + reason: "failed to parse field", + }, + }, + }, + ], + }, + })), + }; + + await handle({ + getFindings, + scan, + now: testDate, + tenant: "default", + client: es7ClientWithErrors, + }); + + expect(consoleErrorSpy).toHaveBeenCalledWith("Bulk Request had errors:"); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.objectContaining({ + errors: true, + items: expect.any(Array), + }), + ); + + consoleErrorSpy.mockRestore(); + consoleLogSpy.mockRestore(); +});