From 25ae24b7483dc7967aaf0b3badde181508150d57 Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Sat, 15 Feb 2025 02:50:11 +0400 Subject: [PATCH] fix(reporter): dynamic commit SHA retrieval --- package-lock.json | 151 ++++++++++++++++-- package.json | 2 +- .../github/GitHubCheckRunReporter.spec.ts | 116 ++++++++++++++ .../github/api/GitHubApiClient.spec.ts | 74 +++++++++ .../src/reporters/github/api/register.spec.ts | 123 ++++++++++++++ .../src/reporters/github/api/register.ts | 23 ++- .../builders/MultiItemsPayloadBuilder.spec.ts | 88 ++++++++++ .../builders/SingleItemPayloadBuilder.spec.ts | 97 +++++++++++ packages/runner/src/lib/SecRunner.ts | 8 +- 9 files changed, 658 insertions(+), 24 deletions(-) create mode 100644 packages/reporter/src/reporters/github/GitHubCheckRunReporter.spec.ts create mode 100644 packages/reporter/src/reporters/github/api/GitHubApiClient.spec.ts create mode 100644 packages/reporter/src/reporters/github/api/register.spec.ts create mode 100644 packages/reporter/src/reporters/github/builders/MultiItemsPayloadBuilder.spec.ts create mode 100644 packages/reporter/src/reporters/github/builders/SingleItemPayloadBuilder.spec.ts diff --git a/package-lock.json b/package-lock.json index 5a0e8b80..416d662e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "lint-staged": "^15.2.10", - "nock": "^13.5.5", + "nock": "^14.0.1", "nx": "20.3.3", "prettier": "^3.3.3", "semantic-release": "^22.0.12", @@ -3333,6 +3333,24 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -4264,6 +4282,31 @@ "@octokit/openapi-types": "^22.2.0" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -7086,10 +7129,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -10124,6 +10168,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12502,18 +12553,18 @@ "license": "MIT" }, "node_modules/nock": { - "version": "13.5.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", - "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.1.tgz", + "integrity": "sha512-IJN4O9pturuRdn60NjQ7YkFt6Rwei7ZKaOwb1tvUIIqTgeD0SDDAX3vrqZD4wcXczeEy/AsUXxpGpP/yHqV7xg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.0", + "@mswjs/interceptors": "^0.37.3", "json-stringify-safe": "^5.0.1", "propagate": "^2.0.0" }, "engines": { - "node": ">= 10.13" + "node": ">=18.20.0 <20 || >=20.12.1" } }, "node_modules/node-emoji": { @@ -15599,6 +15650,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-each-series": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", @@ -17753,6 +17811,13 @@ "mixme": "^0.5.1" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -21619,6 +21684,20 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "requires": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + } + }, "@napi-rs/wasm-runtime": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", @@ -22229,6 +22308,28 @@ "@octokit/openapi-types": "^22.2.0" } }, + "@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "requires": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, "@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -24228,9 +24329,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -26268,6 +26369,12 @@ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==" }, + "is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -27877,12 +27984,12 @@ "dev": true }, "nock": { - "version": "13.5.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", - "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.1.tgz", + "integrity": "sha512-IJN4O9pturuRdn60NjQ7YkFt6Rwei7ZKaOwb1tvUIIqTgeD0SDDAX3vrqZD4wcXczeEy/AsUXxpGpP/yHqV7xg==", "dev": true, "requires": { - "debug": "^4.1.0", + "@mswjs/interceptors": "^0.37.3", "json-stringify-safe": "^5.0.1", "propagate": "^2.0.0" } @@ -29954,6 +30061,12 @@ "wcwidth": "^1.0.1" } }, + "outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, "p-each-series": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", @@ -31447,6 +31560,12 @@ "mixme": "^0.5.1" } }, + "strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/package.json b/package.json index 266ebaa0..bde089d9 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "lint-staged": "^15.2.10", - "nock": "^13.5.5", + "nock": "^14.0.1", "nx": "20.3.3", "prettier": "^3.3.3", "semantic-release": "^22.0.12", diff --git a/packages/reporter/src/reporters/github/GitHubCheckRunReporter.spec.ts b/packages/reporter/src/reporters/github/GitHubCheckRunReporter.spec.ts new file mode 100644 index 00000000..11d36de0 --- /dev/null +++ b/packages/reporter/src/reporters/github/GitHubCheckRunReporter.spec.ts @@ -0,0 +1,116 @@ +import 'reflect-metadata'; +import { GitHubCheckRunReporter } from './GitHubCheckRunReporter'; +import { GitHubClient, GITHUB_CLIENT, GITHUB_CONFIG } from './api'; +import { HttpMethod, Issue, Scan, Severity } from '@sectester/scan'; +import { container } from 'tsyringe'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; + +const issue: Issue = { + id: 'pDzxcEXQC8df1fcz1QwPf9', + order: 1, + details: 'Cross-site request forgery is a type of malicious website exploit.', + name: 'Database connection crashed', + severity: Severity.MEDIUM, + protocol: 'http', + remedy: + 'The best way to protect against those kind of issues is making sure the Database resources are sufficient', + cvss: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L', + time: new Date(), + originalRequest: { + method: HttpMethod.GET, + url: 'https://brokencrystals.com/' + }, + request: { + method: HttpMethod.GET, + url: 'https://brokencrystals.com/' + }, + link: 'https://app.neuralegion.com/scans/pDzxcEXQC8df1fcz1QwPf9/issues/pDzxcEXQC8df1fcz1QwPf9' +}; + +describe('GitHubCheckRunReporter', () => { + let reporter: GitHubCheckRunReporter; + const mockedScan = mock(); + const mockedGitHubClient = mock(); + + const mockConfig = { + token: 'test-token', + repository: 'owner/repo', + commitSha: 'abc123' + }; + + beforeEach(() => { + container.clearInstances(); + + container.register(GITHUB_CONFIG, { useValue: mockConfig }); + container.register(GITHUB_CLIENT, { + useValue: instance(mockedGitHubClient) + }); + + reporter = container.resolve(GitHubCheckRunReporter); + }); + + afterEach(() => { + reset(mockedScan); + reset(mockedGitHubClient); + }); + + describe('constructor', () => { + it('should throw error if token is not set', () => { + expect( + () => + new GitHubCheckRunReporter( + { ...mockConfig, token: '' }, + instance(mockedGitHubClient) + ) + ).toThrow('GitHub token is not set'); + }); + + it('should throw error if repository is not set', () => { + expect( + () => + new GitHubCheckRunReporter( + { ...mockConfig, repository: '' }, + instance(mockedGitHubClient) + ) + ).toThrow('GitHub repository is not set'); + }); + + it('should throw error if commitSha is not set', () => { + expect( + () => + new GitHubCheckRunReporter( + { ...mockConfig, commitSha: '' }, + instance(mockedGitHubClient) + ) + ).toThrow('GitHub commitSha is not set'); + }); + }); + + describe('report', () => { + it('should not create check run if there are no issues', async () => { + when(mockedScan.issues()).thenResolve([]); + + await reporter.report(instance(mockedScan)); + + verify(mockedGitHubClient.createCheckRun(anything())).never(); + }); + + it('should create check run with single issue', async () => { + when(mockedScan.issues()).thenResolve([issue] as Issue[]); + when(mockedGitHubClient.createCheckRun(anything())).thenResolve(); + + await reporter.report(instance(mockedScan)); + + verify(mockedGitHubClient.createCheckRun(anything())).once(); + }); + + it('should create check run with multiple issues', async () => { + when(mockedScan.issues()).thenResolve([issue, issue] as Issue[]); + when(mockedGitHubClient.createCheckRun(anything())).thenResolve(); + + await reporter.report(instance(mockedScan)); + + verify(mockedGitHubClient.createCheckRun(anything())).once(); + }); + }); +}); diff --git a/packages/reporter/src/reporters/github/api/GitHubApiClient.spec.ts b/packages/reporter/src/reporters/github/api/GitHubApiClient.spec.ts new file mode 100644 index 00000000..fec8503a --- /dev/null +++ b/packages/reporter/src/reporters/github/api/GitHubApiClient.spec.ts @@ -0,0 +1,74 @@ +import 'reflect-metadata'; +import { GitHubApiClient } from './GitHubApiClient'; +import { CheckRunPayload, GitHubConfig } from '../types'; +import nock from 'nock'; + +describe('GitHubApiClient', () => { + let client: GitHubApiClient; + const mockConfig: GitHubConfig = { + token: 'test-token', + repository: 'owner/repo', + commitSha: 'abc123' + }; + + beforeAll(() => { + nock.disableNetConnect(); + nock.enableNetConnect('127.0.0.1'); + }); + afterAll(() => nock.enableNetConnect()); + + beforeEach(() => { + if (!nock.isActive()) { + nock.activate(); + } + + client = new GitHubApiClient(mockConfig); + }); + + afterEach(() => { + nock.cleanAll(); + nock.restore(); + }); + + describe('createCheckRun', () => { + const mockPayload: CheckRunPayload = { + name: 'test-check', + head_sha: mockConfig.commitSha, + status: 'completed', + conclusion: 'success', + output: { + title: 'Test Results', + summary: 'All tests passed' + } + }; + + it('should successfully create a check run', async () => { + // Arrange + const scope = nock('https://api.github.com') + .post(`/repos/${mockConfig.repository}/check-runs`) + .matchHeader('authorization', `Bearer ${mockConfig.token}`) + .matchHeader('accept', 'application/vnd.github.v3+json') + .matchHeader('content-type', 'application/json') + .reply(201); + + // Act + await client.createCheckRun(mockPayload); + + // Assert + expect(scope.isDone()).toBe(true); + }); + + it('should throw error when API request fails', async () => { + // Arrange + const scope = nock('https://api.github.com') + .post(`/repos/${mockConfig.repository}/check-runs`) + .reply(401, {}, { statusText: 'Unauthorized' }); + + // Act & Assert + await expect(client.createCheckRun(mockPayload)).rejects.toThrow( + 'GitHub API error: 401 Unauthorized' + ); + expect(scope.isDone()).toBe(true); + }); + }); +}); diff --git a/packages/reporter/src/reporters/github/api/register.spec.ts b/packages/reporter/src/reporters/github/api/register.spec.ts new file mode 100644 index 00000000..fbbbe034 --- /dev/null +++ b/packages/reporter/src/reporters/github/api/register.spec.ts @@ -0,0 +1,123 @@ +import 'reflect-metadata'; +import { GITHUB_CONFIG } from './GitHubConfig'; +import { container } from 'tsyringe'; +import { reset, spy, when } from 'ts-mockito'; + +describe('GitHub Register', () => { + const processEnv = process.env; + let processSpy!: NodeJS.Process; + + beforeEach(() => { + processSpy = spy(process); + }); + + afterEach(() => { + reset(processSpy); + jest.resetModules(); + }); + + it('should register config for check_suite event', async () => { + when(processSpy.env).thenReturn({ + ...processEnv, + GITHUB_EVENT_PATH: '/tmp/event.json', + GITHUB_EVENT_NAME: 'check_suite', + GITHUB_TOKEN: 'mock-token', + GITHUB_REPOSITORY: 'owner/repo' + }); + + jest.mock( + '/tmp/event.json', + () => ({ + check_suite: { head_sha: 'test-sha' } + }), + { virtual: true } + ); + + await import('./register'); + + const config = container.resolve(GITHUB_CONFIG); + expect(config).toEqual({ + commitSha: 'test-sha', + token: 'mock-token', + repository: 'owner/repo' + }); + }); + + it('should register config for check_run event', async () => { + when(processSpy.env).thenReturn({ + ...processEnv, + GITHUB_EVENT_PATH: '/tmp/event.json', + GITHUB_EVENT_NAME: 'check_run', + GITHUB_TOKEN: 'mock-token', + GITHUB_REPOSITORY: 'owner/repo' + }); + + jest.mock( + '/tmp/event.json', + () => ({ + check_run: { + check_suite: { head_sha: 'test-sha' } + } + }), + { virtual: true } + ); + + await import('./register'); + + const config = container.resolve(GITHUB_CONFIG); + expect(config).toEqual({ + commitSha: 'test-sha', + token: 'mock-token', + repository: 'owner/repo' + }); + }); + + it('should register config for pull_request event', async () => { + when(processSpy.env).thenReturn({ + ...processEnv, + GITHUB_EVENT_PATH: '/tmp/event.json', + GITHUB_EVENT_NAME: 'pull_request', + GITHUB_TOKEN: 'mock-token', + GITHUB_REPOSITORY: 'owner/repo' + }); + + jest.mock( + '/tmp/event.json', + () => ({ + pull_request: { + head: { sha: 'test-sha' } + } + }), + { virtual: true } + ); + + await import('./register'); + + const config = container.resolve(GITHUB_CONFIG); + expect(config).toEqual({ + commitSha: 'test-sha', + token: 'mock-token', + repository: 'owner/repo' + }); + }); + + it('should throw error for unsupported event', async () => { + when(processSpy.env).thenReturn({ + ...processEnv, + GITHUB_EVENT_PATH: '/tmp/event.json', + GITHUB_EVENT_NAME: 'unsupported' + }); + + jest.mock( + '/tmp/event.json', + () => ({ + unsupported: {} + }), + { virtual: true } + ); + + await expect(import('./register')).rejects.toThrow( + 'No pull-request and commit data available for the request.' + ); + }); +}); diff --git a/packages/reporter/src/reporters/github/api/register.ts b/packages/reporter/src/reporters/github/api/register.ts index 3c20becf..10e0310d 100644 --- a/packages/reporter/src/reporters/github/api/register.ts +++ b/packages/reporter/src/reporters/github/api/register.ts @@ -3,11 +3,30 @@ import { GITHUB_CONFIG } from './GitHubConfig'; import { GitHubApiClient } from './GitHubApiClient'; import { container } from 'tsyringe'; +let commitSha: string | undefined; + +if (process.env.GITHUB_EVENT_PATH) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const eventData = require(process.env.GITHUB_EVENT_PATH); + + if (process.env.GITHUB_EVENT_NAME === 'check_suite') { + ({ head_sha: commitSha } = eventData.check_suite ?? {}); + } else if (process.env.GITHUB_EVENT_NAME === 'check_run') { + ({ head_sha: commitSha } = eventData.check_run?.check_suite ?? {}); + } else if (process.env.GITHUB_EVENT_NAME === 'pull_request') { + commitSha = eventData.pull_request.head.sha; + } else { + throw new Error( + 'No pull-request and commit data available for the request.' + ); + } +} + container.register(GITHUB_CONFIG, { useValue: { + commitSha, token: process.env.GITHUB_TOKEN, - repository: process.env.GITHUB_REPOSITORY, - commitSha: process.env.PR_COMMIT_SHA + repository: process.env.GITHUB_REPOSITORY } }); container.register(GITHUB_CLIENT, { useClass: GitHubApiClient }); diff --git a/packages/reporter/src/reporters/github/builders/MultiItemsPayloadBuilder.spec.ts b/packages/reporter/src/reporters/github/builders/MultiItemsPayloadBuilder.spec.ts new file mode 100644 index 00000000..afdf277c --- /dev/null +++ b/packages/reporter/src/reporters/github/builders/MultiItemsPayloadBuilder.spec.ts @@ -0,0 +1,88 @@ +import { MultiItemsPayloadBuilder } from './MultiItemsPayloadBuilder'; +import { HttpMethod, Issue, Severity } from '@sectester/scan'; + +describe('MultiItemsPayloadBuilder', () => { + const createIssue = (severity: Severity, name = 'Test Issue'): Issue => ({ + id: 'test-id', + order: 1, + name, + severity, + details: 'Test details', + remedy: 'Fix it', + protocol: 'http', + cvss: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', + time: new Date(), + originalRequest: { + method: HttpMethod.GET, + url: 'https://example.com/api/test' + }, + request: { + method: HttpMethod.GET, + url: 'https://example.com/api/test' + }, + link: 'https://app.neuralegion.com/scan/test-id' + }); + + it('should build payload with multiple issues of different severities', () => { + const issues = [ + createIssue(Severity.CRITICAL), + createIssue(Severity.HIGH), + createIssue(Severity.MEDIUM), + createIssue(Severity.LOW) + ]; + + const builder = new MultiItemsPayloadBuilder( + issues, + 'commit123', + 'test.spec.ts' + ); + const payload = builder.build(); + + expect(payload).toEqual({ + name: 'SecTester (4 issues)', + head_sha: 'commit123', + conclusion: 'failure', + output: { + title: '4 vulnerabilities detected in application endpoints', + summary: '1 Critical, 1 High, 1 Medium, 1 Low severity issues found', + text: expect.stringContaining('GET /api/test: Test Issue'), + annotations: expect.arrayContaining([ + expect.objectContaining({ + path: 'test.spec.ts', + annotation_level: 'failure' + }) + ]) + } + }); + }); + + it('should build payload with no issues', () => { + const builder = new MultiItemsPayloadBuilder( + [], + 'commit123', + 'test.spec.ts' + ); + const payload = builder.build(); + + expect(payload.output?.summary).toBe('No issues found'); + expect(payload.output?.annotations).toHaveLength(0); + }); + + it('should build payload with multiple issues of same severity', () => { + const issues = [ + createIssue(Severity.HIGH, 'Issue 1'), + createIssue(Severity.HIGH, 'Issue 2'), + createIssue(Severity.HIGH, 'Issue 3') + ]; + + const builder = new MultiItemsPayloadBuilder( + issues, + 'commit123', + 'test.spec.ts' + ); + const payload = builder.build(); + + expect(payload.output?.summary).toBe('3 High severity issues found'); + expect(payload.output?.annotations).toHaveLength(3); + }); +}); diff --git a/packages/reporter/src/reporters/github/builders/SingleItemPayloadBuilder.spec.ts b/packages/reporter/src/reporters/github/builders/SingleItemPayloadBuilder.spec.ts new file mode 100644 index 00000000..ed9477a8 --- /dev/null +++ b/packages/reporter/src/reporters/github/builders/SingleItemPayloadBuilder.spec.ts @@ -0,0 +1,97 @@ +import { SingleItemPayloadBuilder } from './SingleItemPayloadBuilder'; +import { HttpMethod, Issue, Severity } from '@sectester/scan'; + +describe('SingleItemPayloadBuilder', () => { + const baseIssue: Issue = { + id: 'test-id', + order: 1, + name: 'SQL Injection', + severity: Severity.HIGH, + details: 'Test vulnerability details', + remedy: 'Fix the code', + protocol: 'http', + cvss: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', + time: new Date(), + originalRequest: { + method: HttpMethod.POST, + url: 'https://example.com/api/users' + }, + request: { + method: HttpMethod.POST, + url: 'https://example.com/api/users' + }, + link: 'https://app.neuralegion.com/scan/test-id' + }; + + it('should build basic payload without optional fields', () => { + const builder = new SingleItemPayloadBuilder( + baseIssue, + 'commit123', + 'test.spec.ts' + ); + const payload = builder.build(); + + expect(payload).toEqual({ + name: 'SecTester - POST /api/users', + head_sha: 'commit123', + conclusion: 'failure', + output: { + title: 'SQL Injection found at POST /api/users', + summary: expect.stringContaining('Name: SQL Injection\nSeverity: high'), + text: 'Test vulnerability details', + annotations: expect.arrayContaining([ + expect.objectContaining({ + path: 'test.spec.ts', + annotation_level: 'failure', + title: 'SQL Injection', + message: expect.stringContaining('Fix the code') + }) + ]) + } + }); + }); + + it('should include comments in payload details when present', () => { + const issueWithComments = { + ...baseIssue, + comments: [ + { + headline: 'Additional Info', + text: 'Important details', + links: ['https://owasp.org/sql-injection'] + } + ] + }; + + const builder = new SingleItemPayloadBuilder( + issueWithComments, + 'commit123', + 'test.spec.ts' + ); + const payload = builder.build(); + + expect(payload.output?.text).toContain('Additional Info'); + expect(payload.output?.text).toContain('Important details'); + expect(payload.output?.text).toContain('https://owasp.org/sql-injection'); + }); + + it('should include resources in payload details when present', () => { + const issueWithResources = { + ...baseIssue, + resources: [ + 'https://example.com/resource1', + 'https://example.com/resource2' + ] + }; + + const builder = new SingleItemPayloadBuilder( + issueWithResources, + 'commit123', + 'test.spec.ts' + ); + const payload = builder.build(); + + expect(payload.output?.text).toContain('https://example.com/resource1'); + expect(payload.output?.text).toContain('https://example.com/resource2'); + }); +}); diff --git a/packages/runner/src/lib/SecRunner.ts b/packages/runner/src/lib/SecRunner.ts index b4f0a2c0..6dc6d25b 100644 --- a/packages/runner/src/lib/SecRunner.ts +++ b/packages/runner/src/lib/SecRunner.ts @@ -94,11 +94,9 @@ export class SecRunner { }); if (process.env.GITHUB_ACTIONS === 'true') { - if (process.env.PR_COMMIT_SHA) { - configuration.container.register(Reporter, { - useClass: GitHubCheckRunReporter - }); - } + configuration.container.register(Reporter, { + useClass: GitHubCheckRunReporter + }); } else { configuration.container.register(Reporter, { useClass: StdReporter