Skip to content

Commit

Permalink
chore: add secure pr checkout and labels
Browse files Browse the repository at this point in the history
Signed-off-by: Ilona Shishov <[email protected]>
  • Loading branch information
IlonaShishov committed May 23, 2024
1 parent 401aa92 commit 7b521e8
Show file tree
Hide file tree
Showing 14 changed files with 735 additions and 109 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
on: [push]
name: RHDA Github Action Workflow

on:
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]

jobs:
rhda_job:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"dependencies": {
"@actions/artifact": "^2.1.4",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@redhat-actions/action-io-generator": "^1.5.0",
"@rhecosystemappeng/exhort-javascript-api": "^0.1.1-ea.26",
Expand Down
42 changes: 1 addition & 41 deletions src/exhortServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,4 @@ async function stackAnalysisService(pathToManifest, options): Promise<string | e
}
}

/**
* Performes RHDA token validation based on the provided options and displays messages based on the validation status.
* @param options The options for token validation.
* @param source The source for which the token is being validated. Example values: 'Snyk', 'OSS Index'.
* @returns A promise resolving after validating the token.
*/
async function tokenValidationService(options, source): Promise<string> {
try {

// Get token validation status code
const tokenValidationStatus = await exhort.validateToken(options);

if (
tokenValidationStatus === 200
) {
return;
} else if (
tokenValidationStatus === 400
) {
return `Missing token. Please provide a valid ${source} Token in the extension workspace settings. Status: ${tokenValidationStatus}`;
} else if (
tokenValidationStatus === 401
) {
return `Invalid token. Please provide a valid ${source} Token in the extension workspace settings. Status: ${tokenValidationStatus}`;
} else if (
tokenValidationStatus === 403
) {
return `Forbidden. The token does not have permissions. Please provide a valid ${source} Token in the extension workspace settings. Status: ${tokenValidationStatus}`;
} else if (
tokenValidationStatus === 429
) {
return `Too many requests. Rate limit exceeded. Please try again in a little while. Status: ${tokenValidationStatus}`;
} else {
return `Failed to validate token. Status: ${tokenValidationStatus}`;
}
} catch (error) {
return `Failed to validate token, Error: ${error.message}`;
}
}

export { stackAnalysisService, tokenValidationService };
export { stackAnalysisService };
94 changes: 89 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
import * as ghCore from '@actions/core';
import * as github from '@actions/github';
import path from 'path';

import * as utils from './utils/utils.js'
import * as utils from './utils.js'
import { resolveManifestFilePath } from './manifestHandler.js';
import { generateRHDAReport } from './rhda.js';
import { Inputs, Outputs } from './generated/inputs-outputs.js';
import { generateArtifacts } from './artifactHandler.js';
import { handleSarif } from './sarif/handler.js';
import { handlePr } from './pr/handler.js';
import { getOriginalCheckoutBranch, checkoutCleanup } from './pr/checkout.js'
import { PrData } from './pr/types.js'
import { RhdaLabels, addLabelsToPr } from './pr/labels.js'


let prData: PrData | undefined;
let originalCheckoutBranch: string;

async function run(): Promise<void> {

ghCore.info(`Working directory is ${process.cwd()}`);

ghCore.debug(`Runner OS is ${utils.getOS()}`);
ghCore.debug(`Node version is ${process.version}`);

console.log(`CHECKPOINT: start`);
// checkout branch securly when payload originates from a pull request
originalCheckoutBranch = await getOriginalCheckoutBranch();
console.log(`CHECKPOINT: originalCheckoutBranch ${originalCheckoutBranch}`);
prData = await handlePr();

const analysisStartTime = new Date().toISOString();
ghCore.debug(`Analysis started at ${analysisStartTime}`);

let sha;
let ref;

sha = await utils.getEnvVar("GITHUB_SHA");
ref = utils.getEnvVar("GITHUB_REF");
if (prData != null) {
({ sha, ref } = prData);
}
else {
sha = await utils.getCommitSha();
ref = utils.getEnvVar("GITHUB_REF");
}

ghCore.info(`Ref to analyze is "${ref}"`);
ghCore.info(`Commit to analyze is "${sha}"`);
Expand All @@ -43,10 +66,71 @@ async function run(): Promise<void> {

/* Label the PR with the scan status, if applicable */

// if (prData) {
// let resultLabel: string;

// switch (vulSeverity) {
// case "error":
// resultLabel = CrdaLabels.CRDA_FOUND_ERROR;
// break;
// case "warning":
// resultLabel = CrdaLabels.CRDA_FOUND_WARNING;
// break;
// default:
// resultLabel = CrdaLabels.CRDA_SCAN_PASSED;
// break;
// }

// await labels.addLabelsToPr(prData.number, [ resultLabel ]);
// }

// /* Evaluate fail_on and set the workflow step exit code accordingly */

// const failOn = ghCore.getInput(Inputs.FAIL_ON) || "error";

// if (vulSeverity !== "none") {
// if (failOn !== "never") {
// if (failOn === "warning") {
// ghCore.info(
// `Input "${Inputs.FAIL_ON}" is "${failOn}", and at least one warning was found. Failing workflow.`
// );
// ghCore.setFailed(`Found vulnerabilities in the project.`);
// }
// else if (failOn === "error" && vulSeverity === "error") {
// ghCore.info(
// `Input "${Inputs.FAIL_ON}" is "${failOn}", and at least one error was found. Failing workflow.`
// );
// ghCore.setFailed(`Found high severity vulnerabilities in the project.`);
// }
// }
// else {
// ghCore.warning(`Found ${utils.capitalizeFirstLetter(vulSeverity)} level vulnerabilities`);
// ghCore.info(`Input "${Inputs.FAIL_ON}" is "${failOn}". Not failing workflow.`);
// }
// }
// else {
// ghCore.info(`✅ No vulnerabilities were found`);
// }

/* Handle artifacts */

await generateArtifacts([rhdaReportJsonFilePath, rhdaReportSarifFilePath]);

}

run()
run()
.then(() => {
// nothing
})
.catch(async (err) => {
if (prData != null) {
await addLabelsToPr(prData.number, [ RhdaLabels.RHDA_SCAN_FAILED ]);
}
ghCore.setFailed(err.message);
})
.finally(async () => {
if (prData != null) {
console.log(`CHECKPOINT cleaning up checkout now`)
await checkoutCleanup(prData.number, originalCheckoutBranch);
}
});
106 changes: 106 additions & 0 deletions src/pr/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as ghCore from "@actions/core";
import * as github from "@actions/github";
import { Octokit } from "@octokit/core";

import * as types from './types.js'
import * as labels from "./labels.js";
import { RhdaLabels } from "./labels.js";
import { getGhToken, prettifyHttpError } from "../utils.js";

export async function isPrScanApproved(pr: types.PrData): Promise<boolean> {
ghCore.info(`Scan is running in a pull request, checking for approval label...`);

// get author authorization
let prAuthorHasWriteAccess = await canPrAuthorWrite(pr);

// update labels
const availableLabels = await labels.getLabels(pr.number);
if (availableLabels.length !== 0) {
ghCore.info(`Pull request labels are: ${availableLabels.map((s) => `"${s}"`).join(", ")}`);
}
else {
ghCore.info("No labels found");
}

const prAction = github.context.payload.action;
ghCore.info(`Action performed is "${prAction}"`);

if (prAction === "edited" || prAction === "synchronize") {
ghCore.info(`Code change detected`);

let labelsToRemove = labels.findLabelsToRemove(availableLabels);

// if pr author has write access do not remove approved label
if (prAuthorHasWriteAccess) {
labelsToRemove = labelsToRemove.filter(label => label !== RhdaLabels.RHDA_SCAN_APPROVED);
}

if (labelsToRemove.length > 0) {
await labels.removeLabelsFromPr(pr.number, labelsToRemove);
}

if (prAuthorHasWriteAccess) {
return true;
}
ghCore.info(`Adding "${RhdaLabels.RHDA_SCAN_PENDING}" label.`);
await labels.addLabelsToPr(pr.number, [ RhdaLabels.RHDA_SCAN_PENDING ]);

return false;
}

if (availableLabels.includes(RhdaLabels.RHDA_SCAN_APPROVED)) {
if (availableLabels.includes(RhdaLabels.RHDA_SCAN_PENDING)) {
await labels.removeLabelsFromPr(pr.number, [ RhdaLabels.RHDA_SCAN_PENDING ]);
}
ghCore.info(`"${RhdaLabels.RHDA_SCAN_APPROVED}" label is present`);
return true;
}

if (prAuthorHasWriteAccess) {
await labels.addLabelsToPr(pr.number, [ RhdaLabels.RHDA_SCAN_APPROVED ]);

return true;
}

if (!availableLabels.includes(RhdaLabels.RHDA_SCAN_PENDING)) {
await labels.addLabelsToPr(pr.number, [ RhdaLabels.RHDA_SCAN_PENDING ]);
}

return false;
}

// API documentation: https://docs.github.com/en/rest/reference/repos#get-repository-permissions-for-a-user
async function canPrAuthorWrite(pr: types.PrData): Promise<boolean> {
if (!pr.author) {
ghCore.warning(`Failed to determine pull request author`);
return false;
}
ghCore.info(`Pull request author is "${pr.author}"`);

const octokit = new Octokit({ auth: getGhToken() });
const { owner, repo } = github.context.repo;
let authorPermissionResponse;
try {
ghCore.info(`Checking if the user "${pr.author}" has write `
+ `access to repository "${owner}/${repo}"`);
authorPermissionResponse = await octokit.request(
"GET /repos/{owner}/{repo}/collaborators/{username}/permission", {
owner,
repo,
username: pr.author,
}
);
}
catch (err) {
throw prettifyHttpError(err);
}

const permission = authorPermissionResponse.data.permission;
if (permission === "admin" || permission === "write") {
ghCore.info(`User has write access to the repository`);
return true;
}
ghCore.info(`User doesn't has write access to the repository`);

return false;
}
52 changes: 52 additions & 0 deletions src/pr/checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as ghCore from '@actions/core';

import { getGitExecutable, execCommand } from "../utils.js";

export async function getOriginalCheckoutBranch(): Promise<string> {
const { exitCode, stdout, stderr } = await execCommand(getGitExecutable(), [ "branch", "--show-current" ]);
if (exitCode != 0) {
throw(new Error(stderr));
}
return stdout.trim();
}

/**
* Checkout PR code to run the CRDA Analysis on a PR,
* After completion of the scan this created remote and branch
* will be deleted and branch will be checkedout the present branch
*/
export async function checkoutPr(baseRepoUrl: string, prNumber: number): Promise<void> {
const remoteName = getPRRemoteName(prNumber);
const localbranchName = getPRBranchName(prNumber);

ghCore.info(`Adding remote ${baseRepoUrl}`);
await execCommand(getGitExecutable(), [ "remote", "add", remoteName, baseRepoUrl ]);

ghCore.info(`⬇️ Checking out PR #${prNumber} to run RHDA analysis.`);
await execCommand(getGitExecutable(), [ "fetch", remoteName, `pull/${prNumber}/head:${localbranchName}` ]);
await execCommand(getGitExecutable(), [ "checkout", localbranchName ]);
}

// Do cleanup after the crda scan and checkout
// back to the original branch
export async function checkoutCleanup(prNumber: number, originalCheckoutBranch: string): Promise<void> {
const remoteName = getPRRemoteName(prNumber);
const branchName = getPRBranchName(prNumber);

ghCore.info(`Checking out back to ${originalCheckoutBranch} branch.`);
await execCommand(getGitExecutable(), [ "checkout", originalCheckoutBranch ]);

ghCore.info(`Removing the created remote "${remoteName}"`);
await execCommand(getGitExecutable(), [ "remote", "remove", remoteName ]);

ghCore.info(`Removing created branch "${branchName}"`);
await execCommand(getGitExecutable(), [ "branch", "-D", `${branchName}` ]);
}

function getPRRemoteName(prNumber: number): string {
return `remote-${prNumber}`;
}

function getPRBranchName(prNumber: number): string {
return `pr-${prNumber}`;
}
Loading

0 comments on commit 7b521e8

Please sign in to comment.