Skip to content

Commit

Permalink
🎨 Apply Formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
gilday committed Jun 3, 2024
1 parent e52c289 commit 3a55d5f
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 107 deletions.
38 changes: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Pixeebot Code Scanning Tool Integration

GitHub Action for upload code scanning results to [Pixeebot](https://pixee.ai/) so it can fix the issues they found.
GitHub Action for upload code scanning results to [Pixeebot](https://pixee.ai/)
so it can fix the issues they found.

## For Sonar Users

Expand All @@ -9,14 +10,17 @@ to execute when the Sonar GitHub App completes a check. The `sonar-pixeebot.yml`
example workflow includes the requisite configuration and is generic enough to
apply to most repositories without modification.

1. Copy the [example sonar-pixeebot.yml](./examples/sonar-pixeebot.yml) workflow to the repository's `.github/workflows` directory.
1. Copy the [example sonar-pixeebot.yml](./examples/sonar-pixeebot.yml) workflow
to the repository's `.github/workflows` directory.
1. Set the `SONAR_TOKEN` secret. Create a SonarCloud token at
[https://sonarcloud.io/account/security](https://sonarcloud.io/account/security). See
[https://sonarcloud.io/account/security](https://sonarcloud.io/account/security).
See
[Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).

## Inputs

Detailed description of the inputs exposed by the `pixee/upload-tool-results-action`:
Detailed description of the inputs exposed by the
`pixee/upload-tool-results-action`:

```yaml
- uses: pixee/upload-tool-results-action
Expand All @@ -41,24 +45,24 @@ Detailed description of the inputs exposed by the `pixee/upload-tool-results-act
# Token for authenticating requests to DefectDojo.
defectdojo-token:

# Key identifying the DefectDojo product (repository) to be analyzed.
# Key identifying the DefectDojo product (repository) to be analyzed.
defectdojo-product-name:

# Base URL of the DefectDojo API.
defectdojo-api-url:

# Base URL of the Contrast API.
contrast-api-url:

# Unique identifier for the organization in Contrast that needs to be analyzed.
contrast-org-id:

# Unique identifier for the specific application within Contrast.
contrast-app-id:

# Api key for authenticating requests to Contrast.
contrast-api-key:

# Token for authenticating requests to Contrast.
contrast-token:

Expand All @@ -69,7 +73,8 @@ Detailed description of the inputs exposed by the `pixee/upload-tool-results-act
## How Does It Work?
The following diagram illustrates how the action orchestrates the results from Sonar, to Pixeebot, and then back to GitHub.
The following diagram illustrates how the action orchestrates the results from
Sonar, to Pixeebot, and then back to GitHub.
```mermaid
sequenceDiagram
Expand All @@ -86,7 +91,8 @@ sequenceDiagram
Pixeebot-->>GitHub: Automatically Fix Issues
```
The code scanning results will feed both Pixeebot's _continuous improvement_ and _pull request hardening_ features.
The code scanning results will feed both Pixeebot's _continuous improvement_ and
_pull request hardening_ features.
- When the code quality tool finds issues on an open PR, Pixeebot opens another
PR to fix those issues.
Expand All @@ -96,7 +102,9 @@ The code scanning results will feed both Pixeebot's _continuous improvement_ and
## Example
The following represents an example GitHub Actions workflow that uploads SonarCloud results to Pixeebot. It runs each time the SonarCloud GitHub App completes a check:
The following represents an example GitHub Actions workflow that uploads
SonarCloud results to Pixeebot. It runs each time the SonarCloud GitHub App
completes a check:
```yaml
name: "Publish Sonar JSON to Pixee"
Expand All @@ -121,4 +129,8 @@ jobs:
sonar-component-key: ${{ secrets.SONAR_COMPONENT_KEY }}
```
Note the use of the repository secrets `SONAR_TOKEN` and `SONAR_COMPONENT_KEY`. The `SONAR_TOKEN` secret is required for private repositories. The `SONAR_COMPONENT_KEY` secret is optional and only necessary if deviating from SonarCloud's established convention. If used, each secret must be defined in the repository's settings.
Note the use of the repository secrets `SONAR_TOKEN` and `SONAR_COMPONENT_KEY`.
The `SONAR_TOKEN` secret is required for private repositories. The
`SONAR_COMPONENT_KEY` secret is optional and only necessary if deviating from
SonarCloud's established convention. If used, each secret must be defined in the
repository's settings.
19 changes: 11 additions & 8 deletions __tests__/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("action", () => {
.spyOn(github, "getRepositoryInfo")
.mockImplementation();
retrieveSonarCloudIssuesMock.mockResolvedValue({ total: 1 });
retrieveSonarCloudHotspotsMock.mockResolvedValue({ paging: {total: 1} });
retrieveSonarCloudHotspotsMock.mockResolvedValue({ paging: { total: 1 } });
});

it("triggers PR analysis when the PR number is available", async () => {
Expand All @@ -67,7 +67,7 @@ describe("action", () => {
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo"
repo: "repo",
});
triggerPrAnalysisMock.mockResolvedValue(undefined);

Expand Down Expand Up @@ -95,12 +95,15 @@ describe("action", () => {
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo"
repo: "repo",
});

await run();

expect(uploadInputFileMock).toHaveBeenCalledWith("sonar_issues", "file.json");
expect(uploadInputFileMock).toHaveBeenCalledWith(
"sonar_issues",
"file.json",
);
});

it("should upload the given semgrep file", async () => {
Expand All @@ -121,7 +124,7 @@ describe("action", () => {
});
getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo"
repo: "repo",
});

await run();
Expand All @@ -147,7 +150,7 @@ describe("action", () => {
sha: "sha",
});

expect(run()).rejects.toThrow("Tool \"semgrep\" requires a file input");
expect(run()).rejects.toThrow('Tool "semgrep" requires a file input');
});

it("should retrieve the SonarCloud results, when the tool is Sonar", async () => {
Expand All @@ -167,7 +170,7 @@ describe("action", () => {

getRepositoryInfoMock.mockReturnValue({
owner: "owner",
repo: "repo"
repo: "repo",
});

await run();
Expand All @@ -176,7 +179,7 @@ describe("action", () => {
expect(retrieveSonarCloudHotspotsMock).toHaveBeenCalled();
expect(uploadInputFileMock).toHaveBeenCalledWith(
"sonar_issues",
expect.stringMatching(/sonar-issues.json$/)
expect.stringMatching(/sonar-issues.json$/),
);
});
});
Expand Down
2 changes: 1 addition & 1 deletion __tests__/pixee-platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("pixee-platform", () => {
Authorization: "Bearer token", // Assert the authorization header
"content-type": expect.stringContaining("multipart/form-data"), // Assert the content type header
},
}
},
);
});
});
4 changes: 3 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ inputs:
description: Base URL of the Contrast API.
required: false
contrast-org-id:
description: Unique identifier for the organization in Contrast that needs to be analyzed.
description:
Unique identifier for the organization in Contrast that needs to be
analyzed.
required: false
contrast-app-id:
description: Unique identifier for the specific application within Contrast.
Expand Down
49 changes: 30 additions & 19 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import * as core from "@actions/core";
import fs from "fs";
import { Tool, getTool } from "./inputs";
import { triggerPrAnalysis, uploadInputFile } from "./pixee-platform";
import { SONAR_RESULT, getSonarCloudInputs, retrieveSonarCloudHotspots, retrieveSonarCloudIssues } from "./sonar";
import {
SONAR_RESULT,
getSonarCloudInputs,
retrieveSonarCloudHotspots,
retrieveSonarCloudIssues,
} from "./sonar";
import { getDefectDojoInputs, retrieveDefectDojoResults } from "./defect-dojo";
import { getGitHubContext, getTempDir } from "./github";
import {getContrastInputs, retrieveContrastResults} from "./contrast";
import { getContrastInputs, retrieveContrastResults } from "./contrast";

/**
* Runs the action.
Expand All @@ -17,7 +22,7 @@ import {getContrastInputs, retrieveContrastResults} from "./contrast";
export async function run() {
const tool = getTool();

switch(tool){
switch (tool) {
case "contrast":
const contrastFile = await fetchOrLocateContrastResultsFile();
await uploadInputFile(tool, contrastFile);
Expand All @@ -29,11 +34,11 @@ export async function run() {
core.info(`Uploaded ${file} to Pixeebot for analysis`);
break;
case "sonar":
const issuesfile = await fetchOrLocateSonarResultsFile("issues");
const issuesfile = await fetchOrLocateSonarResultsFile("issues");
await uploadInputFile("sonar_issues", issuesfile);
core.info(`Uploaded ${issuesfile} to Pixeebot for analysis`);

const hotspotFile = await fetchOrLocateSonarResultsFile("hotspots");
const hotspotFile = await fetchOrLocateSonarResultsFile("hotspots");
await uploadInputFile("sonar_hotspots", hotspotFile);
core.info(`Uploaded ${hotspotFile} to Pixeebot for analysis`);
break;
Expand All @@ -55,11 +60,9 @@ export async function run() {
}

async function fetchOrLocateDefectDojoResultsFile() {

let results = await fetchDefectDojoFindings();
let fileName = "defectdojo.findings.json";


return fetchOrLocateResultsFile("defectdojo", results, fileName);
}

Expand All @@ -70,14 +73,22 @@ async function fetchOrLocateContrastResultsFile() {
return fetchOrLocateResultsFile("contrast", results, fileName, false);
}

async function fetchOrLocateSonarResultsFile(resultType : SONAR_RESULT) {
let results = resultType == "issues" ? await fetchSonarCloudIssues() : await fetchSonarCloudHotspots();
async function fetchOrLocateSonarResultsFile(resultType: SONAR_RESULT) {
let results =
resultType == "issues"
? await fetchSonarCloudIssues()
: await fetchSonarCloudHotspots();
let fileName = `sonar-${resultType}.json`;

return fetchOrLocateResultsFile("sonar", results, fileName);
}

async function fetchOrLocateResultsFile(tool: Tool, results: any, fileName: string, stringifyResults: boolean = true) {
async function fetchOrLocateResultsFile(
tool: Tool,
results: any,
fileName: string,
stringifyResults: boolean = true,
) {
let file = core.getInput("file");
if (file !== "") {
return file;
Expand All @@ -95,42 +106,42 @@ async function fetchOrLocateResultsFile(tool: Tool, results: any, fileName: stri
return file;
}

async function fetchSonarCloudIssues(){
async function fetchSonarCloudIssues() {
const sonarCloudInputs = getSonarCloudInputs();
const results = await retrieveSonarCloudIssues(sonarCloudInputs);
core.info(
`Found ${results.total} SonarCloud issues for component ${sonarCloudInputs.componentKey}`
`Found ${results.total} SonarCloud issues for component ${sonarCloudInputs.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 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.`,
);
}

return results;
}

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

return results;
}

async function fetchDefectDojoFindings(){
async function fetchDefectDojoFindings() {
const inputs = getDefectDojoInputs();
const findings = await retrieveDefectDojoResults(inputs);
const findings = await retrieveDefectDojoResults(inputs);
core.info(
`Found ${findings.count} DefectDojo findings for component ${inputs.productName}`
`Found ${findings.count} DefectDojo findings for component ${inputs.productName}`,
);

return findings;
}

async function fetchContrastFindings(): Promise<any>{
async function fetchContrastFindings(): Promise<any> {
const inputs = getContrastInputs();
const results = await retrieveContrastResults(inputs);

Expand Down
29 changes: 13 additions & 16 deletions src/contrast.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,41 @@
import * as core from "@actions/core";
import axios from "axios";
import JSZip from 'jszip';

import JSZip from "jszip";

export async function retrieveContrastResults(
contrastInputs: ContrastInputs
contrastInputs: ContrastInputs,
): Promise<any> {
const { token, apiKey } = contrastInputs
const { token, apiKey } = contrastInputs;
const url = buildContrastUrl(contrastInputs);
core.info(`Retrieving contrast results from ${url}`);

return axios
.post(url, null,{
.post(url, null, {
headers: {
Authorization: token,
'API-Key': apiKey,
Accept: 'application/x-zip-compressed'
"API-Key": apiKey,
Accept: "application/x-zip-compressed",
},
responseType: 'arraybuffer',
responseType: "arraybuffer",
})
.then(async (response) => {
try {
const zip = await JSZip.loadAsync(response.data);

const xmlFileName = Object.keys(zip.files)[0];
const xmlContent = await zip.file(xmlFileName)?.async('string');
const xmlContent = await zip.file(xmlFileName)?.async("string");

if (core.isDebug()) {
core.info(
`Retrieved contrast results: ${xmlContent}`
);
core.info(`Retrieved contrast results: ${xmlContent}`);
}
return xmlContent;
} catch (error) {
console.error('Error extracting ZIP file:', error);
console.error("Error extracting ZIP file:", error);
throw error;
}
})
.catch(error => {
console.error('Error fetching the ZIP file:', error);
.catch((error) => {
console.error("Error fetching the ZIP file:", error);
throw error;
});
}
Expand All @@ -62,7 +59,7 @@ export function getContrastInputs(): ContrastInputs {
}

function buildContrastUrl(inputs: ContrastInputs): string {
const { apiUrl, orgId, appId } = inputs
const { apiUrl, orgId, appId } = inputs;

return `${apiUrl}/Contrast/api/ng/${orgId}/traces/${appId}/export/xml/all`;
}
Loading

0 comments on commit 3a55d5f

Please sign in to comment.