Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISS-1378 Make pixee/upload-tool-results-action Sonar Host URL Consistent With Sonar Actions #26

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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