Skip to content

Commit

Permalink
ISS-1378 Make pixee/upload-tool-results-action Sonar Host URL Consist…
Browse files Browse the repository at this point in the history
…ent With Sonar Actions (#26)

**Updated**

* The input name was changed from sonar-api-url to sonar-host-url to be
consistent with Sonar tools.
* The action was updated to tolerate trailing slashes in the host URL.
* Documentation and examples were updated.

**Evidence**
*
https://github.com/JesusCotlamee/test-sonar-integration-with-app/actions/runs/9419565450/job/25949593733
  • Loading branch information
JesusCotlamee authored Jun 12, 2024
1 parent 157c02a commit c0d4409
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 89 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ GitHub Action workflow that includes SonarQube, the step that performs the
SonarQube analysis will be followed by a step that applies the SonarQube Quality
Gate. The `pixee/upload-tool-results-action` should follow the SonarQube Quality
Gate. The workflow should be configured to run the
`pixee/upload-tool-results-action` step regardless of the outcome of the
quality gate, so that Pixeebot may fix the issues preventing the quality gate
from passing.
`pixee/upload-tool-results-action` step regardless of the outcome of the quality
gate, so that Pixeebot may fix the issues preventing the quality gate from
passing.

The `pixee/upload-tool-results-action` requires a SonarQube _user token_ token
that is permitted to read Security Hotspots. Typically, the `SONAR_TOKEN` secret
Expand All @@ -49,7 +49,7 @@ confusing it for the typical _project analysis token_.
if: always() && steps.sonarqube-analysis.outcome == 'success'
with:
tool: sonar
sonar-api-url: ${{ vars.SONAR_HOST_URL }}/api
sonar-host-url: ${{ vars.SONAR_HOST_URL }}
sonar-token: ${{ secrets.PIXEE_SONAR_TOKEN }}
sonar-component-key: "<insert-my-sonar-project-key>"
```
Expand Down Expand Up @@ -102,17 +102,17 @@ Detailed description of the inputs exposed by the
# Required
tool:
# Token for authenticating requests to SonarCloud.
# Token for authenticating requests to Sonar.
# Required, when tool is "sonar" and "file" has not been set. Only required for private repository.
sonar-token:
# Key identifying the SonarCloud component to be analyzed. Only necessary if deviating from SonarCloud's established convention.
# Key identifying the Sonar component to be analyzed. Only necessary if deviating from Sonar's established convention.
# Default: `owner_repo`
sonar-component-key:

# Base URL of the Sonar API. Use this to switch from SonarCloud to SonarQube.
# Default: https://sonarcloud.io/api
sonar-api-url:
# SonarCloud or SonarQube host URL. Use this to switch from SonarCloud to SonarQube.
# Default: https://sonarcloud.io
sonar-host-url:

# Token for authenticating requests to DefectDojo.
defectdojo-token:
Expand Down Expand Up @@ -142,7 +142,7 @@ Detailed description of the inputs exposed by the
# Default: https://api.pixee.ai
pixee-api-url:

# Path to the tool's results file to upload to Pixeebot. This does not apply to SonarCloud integration, because the action retrieves the results directly from SonarCloud.
# Path to the tool's results file to upload to Pixeebot. This does not apply to Sonar integration, because the action retrieves the results directly from Sonar.
# Required, when `tool` is not "sonar"
file:
```
Expand Down
26 changes: 13 additions & 13 deletions __tests__/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ let getInputMock: jest.SpiedFunction<typeof core.getInput>;
let getGitHubContextMock: jest.SpiedFunction<typeof github.getGitHubContext>;
let getTempDir: jest.SpiedFunction<typeof github.getTempDir>;
let uploadInputFileMock: jest.SpiedFunction<typeof pixee.uploadInputFile>;
let retrieveSonarCloudIssuesMock: jest.SpiedFunction<
typeof sonar.retrieveSonarCloudIssues
let retrieveSonarIssuesMock: jest.SpiedFunction<
typeof sonar.retrieveSonarIssues
>;
let retrieveSonarCloudHotspotsMock: jest.SpiedFunction<
typeof sonar.retrieveSonarCloudHotspots
let retrieveSonarHotspotsMock: jest.SpiedFunction<
typeof sonar.retrieveSonarHotspots
>;
let triggerPrAnalysisMock: jest.SpiedFunction<typeof pixee.triggerPrAnalysis>;
let getRepositoryInfoMock: jest.SpiedFunction<typeof github.getRepositoryInfo>;
Expand All @@ -35,17 +35,17 @@ describe("action", () => {
triggerPrAnalysisMock = jest
.spyOn(pixee, "triggerPrAnalysis")
.mockImplementation();
retrieveSonarCloudIssuesMock = jest
.spyOn(sonar, "retrieveSonarCloudIssues")
retrieveSonarIssuesMock = jest
.spyOn(sonar, "retrieveSonarIssues")
.mockImplementation();
retrieveSonarCloudHotspotsMock = jest
.spyOn(sonar, "retrieveSonarCloudHotspots")
retrieveSonarHotspotsMock = jest
.spyOn(sonar, "retrieveSonarHotspots")
.mockImplementation();
getRepositoryInfoMock = jest
.spyOn(github, "getRepositoryInfo")
.mockImplementation();
retrieveSonarCloudIssuesMock.mockResolvedValue({ total: 1 });
retrieveSonarCloudHotspotsMock.mockResolvedValue({ paging: { total: 1 } });
retrieveSonarIssuesMock.mockResolvedValue({ total: 1 });
retrieveSonarHotspotsMock.mockResolvedValue({ paging: { total: 1 } });
});

it("triggers PR analysis when the PR number is available", async () => {
Expand Down Expand Up @@ -153,7 +153,7 @@ describe("action", () => {
expect(run()).rejects.toThrow('Tool "semgrep" requires a file input');
});

it("should retrieve the SonarCloud results, when the tool is Sonar", async () => {
it("should retrieve the Sonar results, when the tool is Sonar", async () => {
getInputMock.mockImplementation((name: string) => {
switch (name) {
case "tool":
Expand All @@ -175,8 +175,8 @@ describe("action", () => {

await run();

expect(retrieveSonarCloudIssuesMock).toHaveBeenCalled();
expect(retrieveSonarCloudHotspotsMock).toHaveBeenCalled();
expect(retrieveSonarIssuesMock).toHaveBeenCalled();
expect(retrieveSonarHotspotsMock).toHaveBeenCalled();
expect(uploadInputFileMock).toHaveBeenCalledWith(
"sonar_issues",
expect.stringMatching(/sonar-issues.json$/),
Expand Down
142 changes: 142 additions & 0 deletions __tests__/sonar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as github from "../src/github";
import { buildSonarUrl, SonarInputs } from "../src/sonar";
import { GitHubContext } from "../src/github";

let getGitHubContextMock: jest.SpiedFunction<typeof github.getGitHubContext>;

describe("sonar", () => {
const sonarHostUrl = "https://sonar.io/api";
const path = "api/issues/search";
const componentKey = "myComponent";

const sonarInputs = {
token: "",
sonarHostUrl,
componentKey,
} as SonarInputs;

const githubContext = {
owner: "owner",
repo: "repo",
sha: "sha",
prNumber: 123,
} as GitHubContext;

beforeEach(() => {
jest.clearAllMocks();
getGitHubContextMock = jest
.spyOn(github, "getGitHubContext")
.mockImplementation();
});

it("should build the URL with pullRequest parameter if prNumber exists", async () => {
getGitHubContextMock.mockReturnValue(githubContext);

const result = buildSonarUrl({
sonarInputs,
path,
queryParamKey: "componentKeys",
});

expect(result).toBe(
"https://sonar.io/api/issues/search?componentKeys=myComponent&resolved=false&ps=500&pullRequest=123",
);
});

it("should build the URL without pullRequest parameter if prNumber does not exist", async () => {
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
prNumber: undefined,
});

const result = buildSonarUrl({
sonarInputs,
path,
queryParamKey: "componentKeys",
});

expect(result).toBe(
"https://sonar.io/api/issues/search?componentKeys=myComponent&resolved=false&ps=500",
);
});

it("should build the URL correctly with queryParamKey projectKey", async () => {
getGitHubContextMock.mockReturnValue(githubContext);

const result = buildSonarUrl({
sonarInputs,
path,
queryParamKey: "projectKey",
});

expect(result).toBe(
"https://sonar.io/api/issues/search?projectKey=myComponent&resolved=false&ps=500&pullRequest=123",
);
});

it("should encode the componentKey properly", async () => {
const specialComponentKey = "myComponent with spaces";
getGitHubContextMock.mockReturnValue(githubContext);

const sonarInputs = {
token: "",
sonarHostUrl,
componentKey: specialComponentKey,
} as SonarInputs;

const result = buildSonarUrl({
sonarInputs,
path,
queryParamKey: "componentKeys",
});

expect(result).toBe(
"https://sonar.io/api/issues/search?componentKeys=myComponent+with+spaces&resolved=false&ps=500&pullRequest=123",
);
});

it("should handle sonarHost with trailing slash correctly", async () => {
const sonarHostWithSlash = "https://sonar.io/";
getGitHubContextMock.mockReturnValue(githubContext);

const sonarInputs = {
token: "",
sonarHostUrl: sonarHostWithSlash,
componentKey,
} as SonarInputs;

const result = buildSonarUrl({
sonarInputs,
path,
queryParamKey: "componentKeys",
});

expect(result).toBe(
"https://sonar.io/api/issues/search?componentKeys=myComponent&resolved=false&ps=500&pullRequest=123",
);
});

it("should build the URL correctly with append the path correctly beyond /context/", async () => {
const sonarHostWithContext = "https://sonar.io/context/";

getGitHubContextMock.mockReturnValue(githubContext);

const sonarInputs = {
token: "",
sonarHostUrl: sonarHostWithContext,
componentKey,
} as SonarInputs;

const result = buildSonarUrl({
sonarInputs,
path,
queryParamKey: "projectKey",
});

expect(result).toBe(
"https://sonar.io/context/api/issues/search?projectKey=myComponent&resolved=false&ps=500&pullRequest=123",
);
});
});
10 changes: 5 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ inputs:
description: Path to the tool's results file to share with Pixeebot.
required: false
sonar-token:
description: Token for authenticating requests to SonarCloud.
description: Token for authenticating requests to Sonar.
required: false
sonar-component-key:
description: Key identifying the SonarCloud component to be analyzed.
description: Key identifying the Sonar component to be analyzed.
required: false
sonar-api-url:
description: Base URL of the SonarCloud API.
default: https://sonarcloud.io/api
sonar-host-url:
description: SonarCloud or SonarQube host URL.
default: https://sonarcloud.io
defectdojo-token:
description: Token for authenticating requests to DefectDojo.
required: false
Expand Down
2 changes: 1 addition & 1 deletion examples/sonarqube-pixeebot-maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ jobs:
if: always() && steps.sonarqube-analysis.outcome == 'success' # run this when the analysis is successful, regardless of the quality gate status
with:
tool: sonar
sonar-api-url: ${{ vars.SONAR_HOST_URL }}/api
sonar-host-url: ${{ vars.SONAR_HOST_URL }}
sonar-token: ${{ secrets.PIXEE_SONAR_TOKEN }}
sonar-component-key: "<insert-my-sonar-project-key>"
2 changes: 1 addition & 1 deletion examples/sonarqube-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ jobs:
if: always() && steps.sonarqube-analysis.outcome == 'success'
with:
tool: sonar
sonar-api-url: ${{ vars.SONAR_HOST_URL }}/api
sonar-host-url: ${{ vars.SONAR_HOST_URL }}
sonar-token: ${{ secrets.PIXEE_SONAR_TOKEN }}
sonar-component-key: "<insert-my-sonar-project-key>"
30 changes: 15 additions & 15 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Tool, getTool } from "./inputs";
import { triggerPrAnalysis, uploadInputFile } from "./pixee-platform";
import {
SONAR_RESULT,
getSonarCloudInputs,
retrieveSonarCloudHotspots,
retrieveSonarCloudIssues,
getSonarInputs,
retrieveSonarHotspots,
retrieveSonarIssues,
} from "./sonar";
import { getDefectDojoInputs, retrieveDefectDojoResults } from "./defect-dojo";
import { getGitHubContext, getTempDir } from "./github";
Expand All @@ -15,7 +15,7 @@ import { getContrastInputs, retrieveContrastResults } from "./contrast";
/**
* Runs the action.
*
* Presently only handles the case where the tool is SonarCloud and the file is not provided and therefore must be retrieved as part of a check_run. We will exapnd this to handle other types of GitHub events.
* Presently only handles the case where the tool is Sonar and the file is not provided and therefore must be retrieved as part of a check_run. We will exapnd this to handle other types of GitHub events.
*
* If the event is associated with a PR, the action will trigger a PR analysis.
*/
Expand Down Expand Up @@ -76,8 +76,8 @@ async function fetchOrLocateContrastResultsFile() {
async function fetchOrLocateSonarResultsFile(resultType: SONAR_RESULT) {
let results =
resultType == "issues"
? await fetchSonarCloudIssues()
: await fetchSonarCloudHotspots();
? await fetchSonarIssues()
: await fetchSonarHotspots();
let fileName = `sonar-${resultType}.json`;

return fetchOrLocateResultsFile("sonar", results, fileName);
Expand Down Expand Up @@ -106,26 +106,26 @@ async function fetchOrLocateResultsFile(
return file;
}

async function fetchSonarCloudIssues() {
const sonarCloudInputs = getSonarCloudInputs();
const results = await retrieveSonarCloudIssues(sonarCloudInputs);
async function fetchSonarIssues() {
const sonarInputs = getSonarInputs();
const results = await retrieveSonarIssues(sonarInputs);
core.info(
`Found ${results.total} SonarCloud issues for component ${sonarCloudInputs.componentKey}`,
`Found ${results.total} Sonar issues for component ${sonarInputs.componentKey}`,
);
if (results.total === 0) {
core.info(
`When the SonarCloud token is incorrect, SonarCloud responds with an empty response indistinguishable from cases where there are no issues. If you expected issues, please check the token.`,
`When the Sonar token is incorrect, Sonar responds with an empty response indistinguishable from cases where there are no issues. If you expected issues, please check the token.`,
);
}

return results;
}

async function fetchSonarCloudHotspots() {
const sonarCloudInputs = getSonarCloudInputs();
const results = await retrieveSonarCloudHotspots(sonarCloudInputs);
async function fetchSonarHotspots() {
const sonarInputs = getSonarInputs();
const results = await retrieveSonarHotspots(sonarInputs);
core.info(
`Found ${results.paging.total} SonarCloud hotspots for component ${sonarCloudInputs.componentKey}`,
`Found ${results.paging.total} Sonar hotspots for component ${sonarInputs.componentKey}`,
);

return results;
Expand Down
Loading

0 comments on commit c0d4409

Please sign in to comment.