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

✨ Add Support for Uploading Local Tool Results Instead of Fetching Results #30

Merged
merged 1 commit into from
Jul 9, 2024
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Detailed description of the inputs exposed by the
tool:

# Token for authenticating requests to Sonar.
# Required, when tool is "sonar" and "file" has not been set. Only required for private repository.
# Required, when tool is "sonar". Only required for private repository.
sonar-token:

# Key identifying the Sonar component to be analyzed. Only necessary if deviating from Sonar's established convention.
Expand Down Expand Up @@ -142,8 +142,9 @@ 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 Sonar integration, because the action retrieves the results directly from Sonar.
# Required, when `tool` is not "sonar"
# Path to the tool's results file to upload to Pixeebot.
# This is required when using a `tool` that does not support automatically fetching results. Contrast, Sonar, and DefectDojo integrations support automatically fetching results. When this input is used with those tools, the given file will be uploaded _instead of_ automatically fetching results.
# Note: for Sonar results, the tool must be `sonar_hotspots` or `sonar_issues` instead of `sonar` when using this input.
file:
```

Expand Down
109 changes: 79 additions & 30 deletions __tests__/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import { run } from "../src/action";
import * as pixee from "../src/pixee-platform";
import * as sonar from "../src/sonar";
import * as github from "../src/github";
import * as contrast from "../src/contrast";
import * as defectdojo from "../src/defect-dojo";

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.uploadInputFiles>;
let retrieveContrastResultsMock: jest.SpiedFunction<
typeof contrast.retrieveContrastResults
>;
let retrieveDefectDojoResultsMock: jest.SpiedFunction<
typeof defectdojo.retrieveDefectDojoResults
>;
let retrieveSonarIssuesMock: jest.SpiedFunction<
typeof sonar.retrieveSonarIssues
>;
Expand All @@ -35,6 +43,12 @@ describe("action", () => {
triggerPrAnalysisMock = jest
.spyOn(pixee, "triggerPrAnalysis")
.mockImplementation();
retrieveContrastResultsMock = jest
.spyOn(contrast, "retrieveContrastResults")
.mockImplementation();
retrieveDefectDojoResultsMock = jest
.spyOn(defectdojo, "retrieveDefectDojoResults")
.mockImplementation();
retrieveSonarIssuesMock = jest
.spyOn(sonar, "retrieveSonarIssues")
.mockImplementation();
Expand All @@ -58,7 +72,7 @@ describe("action", () => {
getInputMock.mockImplementation((name: string) => {
switch (name) {
case "tool":
return "sonar";
return "sonar_issues";
case "file":
return "file.json";
default:
Expand All @@ -77,30 +91,80 @@ describe("action", () => {
});

describe("when the file input is not empty", () => {
it("should upload the given sonar file", async () => {
beforeEach(() => {
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo",
});
});

it("should upload the given file instead of automatically fetching Sonar results", async () => {
getInputMock.mockImplementation((name: string) => {
switch (name) {
case "tool":
return "sonar";
return "sonar_issues";
case "file":
return "file.json";
default:
return "";
}
});
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",

await run();

expect(uploadInputFileMock).toHaveBeenCalledWith(
"sonar_issues",
new Array("file.json"),
);
expect(retrieveSonarIssuesMock).not.toHaveBeenCalled();
expect(retrieveSonarHotspotsMock).not.toHaveBeenCalled();
});

it("should upload the given file instead of automatically fetching Contrast results", async () => {
getInputMock.mockImplementation((name: string) => {
switch (name) {
case "tool":
return "contrast";
case "file":
return "file.json";
default:
return "";
}
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo",

await run();

expect(uploadInputFileMock).toHaveBeenCalledWith(
"contrast",
new Array("file.json"),
);
expect(retrieveContrastResultsMock).not.toHaveBeenCalled();
});

it("should upload the given file instead of automatically fetching DefectDojo results", async () => {
getInputMock.mockImplementation((name: string) => {
switch (name) {
case "tool":
return "defectdojo";
case "file":
return "file.json";
default:
return "";
}
});

await run();

expect(uploadInputFileMock).toHaveBeenCalledWith("sonar_issues", new Array("file.json"));
expect(uploadInputFileMock).toHaveBeenCalledWith(
"defectdojo",
new Array("file.json"),
);
expect(retrieveDefectDojoResultsMock).not.toHaveBeenCalled();
});

it("should upload the given semgrep file", async () => {
Expand All @@ -114,19 +178,13 @@ describe("action", () => {
return "";
}
});
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo",
});

await run();

expect(uploadInputFileMock).toHaveBeenCalledWith("semgrep", new Array("file.json"));
expect(uploadInputFileMock).toHaveBeenCalledWith(
"semgrep",
new Array("file.json"),
);
});

it("should upload the given snyk sarif file", async () => {
Expand All @@ -140,15 +198,6 @@ describe("action", () => {
return "";
}
});
getGitHubContextMock.mockReturnValue({
owner: "owner",
repo: "repo",
sha: "sha",
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo",
});

await run();

Expand Down
12 changes: 6 additions & 6 deletions __tests__/sonar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("sonar", () => {
path,
queryParamKey: "componentKeys",
pageSize: 500,
page: 1
page: 1,
});

expect(result).toBe(
Expand All @@ -58,7 +58,7 @@ describe("sonar", () => {
path,
queryParamKey: "componentKeys",
pageSize: 500,
page: 1
page: 1,
});

expect(result).toBe(
Expand All @@ -74,7 +74,7 @@ describe("sonar", () => {
path,
queryParamKey: "projectKey",
pageSize: 500,
page: 1
page: 1,
});

expect(result).toBe(
Expand All @@ -97,7 +97,7 @@ describe("sonar", () => {
path,
queryParamKey: "componentKeys",
pageSize: 500,
page: 1
page: 1,
});

expect(result).toBe(
Expand All @@ -120,7 +120,7 @@ describe("sonar", () => {
path,
queryParamKey: "componentKeys",
pageSize: 500,
page: 1
page: 1,
});

expect(result).toBe(
Expand All @@ -144,7 +144,7 @@ describe("sonar", () => {
path,
queryParamKey: "projectKey",
pageSize: 500,
page: 1
page: 1,
});

expect(result).toBe(
Expand Down
84 changes: 47 additions & 37 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getContrastInputs, retrieveContrastResults } from "./contrast";

interface SonarResults {
totalResults: number;
results: any
results: any;
}

const MAX_PAGE_SIZE = 500;
Expand All @@ -29,34 +29,41 @@ const MAX_PAGE_SIZE = 500;
export async function run() {
const tool = getTool();

switch (tool) {
case "contrast":
const contrastFile = await fetchOrLocateContrastResultsFile();
await uploadInputFiles(tool, new Array(contrastFile));
core.info(`Uploaded ${contrastFile} to Pixeebot for analysis`);
break;
case "defectdojo":
const file = await fetchOrLocateDefectDojoResultsFile();
await uploadInputFiles(tool, new Array(file));
core.info(`Uploaded ${file} to Pixeebot for analysis`);
break;
case "sonar":
const issuesfiles = await fetchOrLocateSonarResultsFile("issues");
await uploadInputFiles("sonar_issues", issuesfiles);
core.info(`Uploaded ${issuesfiles} to Pixeebot for analysis`);

const hotspotFiles = await fetchOrLocateSonarResultsFile("hotspots");
await uploadInputFiles("sonar_hotspots", hotspotFiles);
core.info(`Uploaded ${hotspotFiles} to Pixeebot for analysis`);
break;
default:
const inputFile = core.getInput("file");
if (!inputFile) {
// if the file input is provided, upload the file
const inputFile = core.getInput("file");
if (inputFile) {
if (tool === "sonar") {
throw new Error(
`Tool "sonar" is too imprecise to use with a file input. Please use "sonar_issues" or "sonar_hotspots" instead.`,
);
}
await uploadInputFiles(tool, new Array(inputFile));
core.info(`Uploaded ${inputFile} for ${tool} to Pixeebot for analysis`);
} else {
// if the file input is not provided, automatically fetch the results and upload them, if supported
switch (tool) {
case "contrast":
const contrastFile = await fetchOrLocateContrastResultsFile();
await uploadInputFiles(tool, new Array(contrastFile));
core.info(`Uploaded ${contrastFile} to Pixeebot for analysis`);
break;
case "defectdojo":
const file = await fetchOrLocateDefectDojoResultsFile();
await uploadInputFiles(tool, new Array(file));
core.info(`Uploaded ${file} to Pixeebot for analysis`);
break;
case "sonar":
const issuesfiles = await fetchOrLocateSonarResultsFile("issues");
await uploadInputFiles("sonar_issues", issuesfiles);
core.info(`Uploaded ${issuesfiles} to Pixeebot for analysis`);

const hotspotFiles = await fetchOrLocateSonarResultsFile("hotspots");
await uploadInputFiles("sonar_hotspots", hotspotFiles);
core.info(`Uploaded ${hotspotFiles} to Pixeebot for analysis`);
break;
default:
throw new Error(`Tool "${tool}" requires a file input`);
}

await uploadInputFiles(tool, new Array(inputFile));
core.info(`Uploaded ${inputFile} for ${tool} to Pixeebot for analysis`);
}
}

const { prNumber } = getGitHubContext();
Expand All @@ -81,9 +88,8 @@ async function fetchOrLocateContrastResultsFile() {
}

async function fetchOrLocateSonarResultsFile(
resultType: SONAR_RESULT
resultType: SONAR_RESULT,
): Promise<Array<string>> {

let page = 1;
const files = new Array();
let isAllResults = false;
Expand All @@ -95,7 +101,11 @@ async function fetchOrLocateSonarResultsFile(
: await fetchSonarHotspots(MAX_PAGE_SIZE, page);
let fileName = `sonar-${resultType}-${page}.json`;

let file = await fetchOrLocateResultsFile("sonar", sonarResults.results, fileName);
let file = await fetchOrLocateResultsFile(
"sonar",
sonarResults.results,
fileName,
);

let total = sonarResults.totalResults;

Expand Down Expand Up @@ -133,8 +143,8 @@ async function fetchOrLocateResultsFile(

async function fetchSonarIssues(
pageSize: number,
page: number
) : Promise<SonarResults>{
page: number,
): Promise<SonarResults> {
const sonarInputs = getSonarInputs();
const results = await retrieveSonarIssues(sonarInputs, pageSize, page);

Expand All @@ -147,20 +157,20 @@ async function fetchSonarIssues(
);
}

return {results, totalResults: results.total};
return { results, totalResults: results.total };
}

async function fetchSonarHotspots(
pageSize: number,
page: number
) : Promise<SonarResults>{
page: number,
): Promise<SonarResults> {
const sonarInputs = getSonarInputs();
const results = await retrieveSonarHotspots(sonarInputs, pageSize, page);
core.info(
`Found ${results.paging.total} Sonar hotspots for component ${sonarInputs.componentKey}`,
);

return {results, totalResults: results.paging.total};
return { results, totalResults: results.paging.total };
}

async function fetchDefectDojoFindings() {
Expand Down
Loading