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 6 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
14 changes: 7 additions & 7 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: ${{ vars.SONAR_HOST_URL }}
sonar-token: ${{ secrets.PIXEE_SONAR_TOKEN }}
sonar-component-key: "<insert-my-sonar-project-key>"
```
Expand Down Expand Up @@ -110,9 +110,9 @@ Detailed description of the inputs exposed by the
# 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:
# Base host of the SonarCloud or SonarQube API. Use this to switch from SonarCloud to SonarQube.
# Default: https://sonarcloud.io
sonar-host:

# Token for authenticating requests to DefectDojo.
defectdojo-token:
Expand Down
94 changes: 94 additions & 0 deletions __tests__/sonar.test.ts
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { buildSonarCloudIssuesUrl } from "../src/sonar";
import * as github from "../src/github";

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

describe("sonar", () => {
const sonarHost = "https://sonarcloud.io/api/";
const componentKey = "myComponent";

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

it("should build the URL with pullRequest parameter if prNumber exists", async () => {
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
prNumber: 123,
});

const result = buildSonarCloudIssuesUrl({
token: "",
sonarHost,
componentKey,
});

expect(result).toBe(
"https://sonarcloud.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 = buildSonarCloudIssuesUrl({
token: "",
sonarHost,
componentKey,
});

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

it("should encode the componentKey properly", async () => {
const specialComponentKey = "myComponent with spaces";
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
prNumber: 123,
});

const result = buildSonarCloudIssuesUrl({
token: "",
sonarHost,
componentKey: specialComponentKey,
});

expect(result).toBe(
"https://sonarcloud.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://sonarcloud.io/";
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
prNumber: 123,
});

const result = buildSonarCloudIssuesUrl({
token: "",
sonarHost: sonarHostWithSlash,
componentKey,
});

expect(result).toBe(
"https://sonarcloud.io/api/issues/search?componentKeys=myComponent&resolved=false&ps=500&pullRequest=123",
);
});
});
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ inputs:
sonar-component-key:
description: Key identifying the SonarCloud component to be analyzed.
required: false
sonar-api-url:
description: Base URL of the SonarCloud API.
default: https://sonarcloud.io/api
sonar-host:
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved
description: Base host of the SonarCloud or SonarQube API.
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: ${{ 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: ${{ vars.SONAR_HOST_URL }}
sonar-token: ${{ secrets.PIXEE_SONAR_TOKEN }}
sonar-component-key: "<insert-my-sonar-project-key>"
85 changes: 60 additions & 25 deletions src/sonar.ts
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,70 @@ export async function retrieveSonarCloudIssues(
return retrieveSonarCloudResults(sonarCloudInputs, url, "issues");
}

export function buildSonarCloudIssuesUrl({
sonarHost,
componentKey,
}: SonarCloudInputs): string {
const { prNumber } = getGitHubContext();
const path = "/api/issues/search";
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved

const queryParams = {
componentKeys: componentKey,
resolved: "false",
ps: MAX_PAGE_SIZE,
...(prNumber && { pullRequest: prNumber }),
};

return buildSonarCloudUrl({ sonarHost, path, queryParams });
}

export async function retrieveSonarCloudHotspots(
sonarCloudInputs: SonarCloudInputs,
): Promise<SonarSearchHotspotResult> {
const url = buildSonarCloudHotspotsUrl(sonarCloudInputs);
return retrieveSonarCloudResults(sonarCloudInputs, url, "hotspots");
}

export function buildSonarCloudHotspotsUrl({
sonarHost,
componentKey,
}: SonarCloudInputs): string {
const { prNumber } = getGitHubContext();
const path = "/api/hotspots/search";
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved

const queryParams = {
projectKey: componentKey,
resolved: "false",
ps: MAX_PAGE_SIZE,
...(prNumber && { pullRequest: prNumber }),
};

return buildSonarCloudUrl({ sonarHost, path, queryParams });
}

export function buildSonarCloudUrl({
sonarHost,
path,
queryParams,
}: {
sonarHost: string;
path: string;
queryParams: { [key: string]: string | number };
}): string {
const baseApiUrl = new URL(sonarHost);
const apiUrl = new URL(path, baseApiUrl);
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved

Object.entries(queryParams).forEach(([key, value]) => {
apiUrl.searchParams.append(key, value.toString());
});

if (core.isDebug()) {
core.info(`SonarCloud url ${apiUrl}`);
JesusCotlamee marked this conversation as resolved.
Show resolved Hide resolved
}

return apiUrl.href;
}

async function retrieveSonarCloudResults(
{ token }: SonarCloudInputs,
url: string,
Expand Down Expand Up @@ -62,38 +119,16 @@ async function retrieveSonarCloudResults(
interface SonarCloudInputs {
token: string;
componentKey: string;
apiUrl: string;
sonarHost: string;
}

export function getSonarCloudInputs(): SonarCloudInputs {
const apiUrl = core.getInput("sonar-api-url", { required: true });
const sonarHost = core.getInput("sonar-host", { required: true });
const token = core.getInput("sonar-token");
let componentKey = core.getInput("sonar-component-key");
if (!componentKey) {
const { owner, repo } = getRepositoryInfo();
componentKey = `${owner}_${repo}`;
}
return { token, componentKey, apiUrl };
}

function buildSonarCloudIssuesUrl({
apiUrl,
componentKey,
}: SonarCloudInputs): string {
const { prNumber } = getGitHubContext();
const url = `${apiUrl}/issues/search?componentKeys=${encodeURIComponent(
componentKey,
)}&resolved=false&ps=${MAX_PAGE_SIZE}`;
return prNumber ? `${url}&pullRequest=${prNumber}` : url;
}

function buildSonarCloudHotspotsUrl({
apiUrl,
componentKey,
}: SonarCloudInputs): string {
const { prNumber } = getGitHubContext();
const url = `${apiUrl}/hotspots/search?projectKey=${encodeURIComponent(
componentKey,
)}&resolved=false&ps=${MAX_PAGE_SIZE}`;
return prNumber ? `${url}&pullRequest=${prNumber}` : url;
return { token, componentKey, sonarHost };
}