Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
67 changes: 67 additions & 0 deletions action/lib/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const utils_1 = require("./utils");
function main() {
return __awaiter(this, void 0, void 0, function* () {
try {
core.startGroup('Frogbot');
let jfrogUrl = yield utils_1.Utils.getJfrogPlatformUrl();
yield utils_1.Utils.setupOidcTokenIfNeeded(jfrogUrl);
const eventName = yield utils_1.Utils.setFrogbotEnv();
yield utils_1.Utils.addToPath();
switch (eventName) {
case 'pull_request':
case 'pull_request_target':
yield utils_1.Utils.execScanPullRequest();
break;
case 'push':
case 'schedule':
case 'workflow_dispatch':
yield utils_1.Utils.execCreateFixPullRequests();
break;
default:
core.setFailed(eventName + ' event is not supported by Frogbot');
}
}
catch (error) {
core.setFailed(error.message);
}
finally {
core.endGroup();
}
});
}
main();
313 changes: 313 additions & 0 deletions action/lib/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Utils = void 0;
const core = __importStar(require("@actions/core"));
const exec_1 = require("@actions/exec");
const github_1 = require("@actions/github");
const tool_cache_1 = require("@actions/tool-cache");
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = require("path");
const simple_git_1 = require("simple-git");
const http_client_1 = require("@actions/http-client");
class Utils {
static addToPath() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let fileName = Utils.getExecutableName();
let version = core.getInput(Utils.VERSION_ARG);
let major = version.split('.')[0];
if (version === this.LATEST_CLI_VERSION_ARG) {
version = Utils.LATEST_RELEASE_VERSION;
major = '3';
}
else {
if (this.loadFromCache(version)) {
// Download is not needed
return;
}
}
// Download Frogbot
const releasesRepo = (_a = process.env.JF_RELEASES_REPO) !== null && _a !== void 0 ? _a : '';
let url = Utils.getCliUrl(major, version, fileName, releasesRepo);
core.debug('Downloading Frogbot from ' + url);
let auth = this.generateAuthString(releasesRepo);
let downloadDir = yield (0, tool_cache_1.downloadTool)(url, '', auth);
// Cache 'frogbot' executable
yield this.cacheAndAddPath(downloadDir, version, fileName);
});
}
static generateAuthString(releasesRepo) {
var _a, _b, _c;
if (!releasesRepo) {
return '';
}
let accessToken = (_a = process.env.JF_ACCESS_TOKEN) !== null && _a !== void 0 ? _a : '';
let username = (_b = process.env.JF_USER) !== null && _b !== void 0 ? _b : '';
let password = (_c = process.env.JF_PASSWORD) !== null && _c !== void 0 ? _c : '';
if (accessToken) {
return 'Bearer ' + Buffer.from(accessToken).toString();
}
else if (username && password) {
return 'Basic ' + Buffer.from(username + ':' + password).toString('base64');
}
return '';
}
static setFrogbotEnv() {
return __awaiter(this, void 0, void 0, function* () {
core.exportVariable('JF_GIT_PROVIDER', 'github');
core.exportVariable('JF_GIT_OWNER', github_1.context.repo.owner);
let owner = github_1.context.repo.repo;
if (owner) {
core.exportVariable('JF_GIT_REPO', owner.substring(owner.indexOf('/') + 1));
}
core.exportVariable('JF_GIT_PULL_REQUEST_ID', github_1.context.issue.number);
if (!process.env.JF_GIT_TOKEN) {
const gitToken = process.env.GITHUB_TOKEN;
if (!gitToken) {
throw new Error('Git token not found. Please ensure GITHUB_TOKEN is available by setting permissions in your workflow, ' +
'or set JF_GIT_TOKEN manually.');
}
core.exportVariable('JF_GIT_TOKEN', gitToken);
}
if (!process.env.JF_GIT_API_ENDPOINT) {
const apiUrl = process.env.GITHUB_API_URL || github_1.context.apiUrl || 'https://api.github.com';
core.exportVariable('JF_GIT_API_ENDPOINT', apiUrl);
}
if (!process.env.JF_GIT_SERVER_URL) {
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
core.exportVariable('JF_GIT_SERVER_URL', serverUrl);
}
return github_1.context.eventName;
});
}
/**
* Execute frogbot scan-pull-request command.
*/
static execScanPullRequest() {
return __awaiter(this, void 0, void 0, function* () {
if (!process.env.JF_GIT_BASE_BRANCH) {
core.exportVariable('JF_GIT_BASE_BRANCH', github_1.context.ref);
}
let res = yield (0, exec_1.exec)(Utils.getExecutableName(), ['scan-pull-request']);
if (res !== core.ExitCode.Success) {
throw new Error('Frogbot exited with exit code ' + res);
}
});
}
/**
* Execute frogbot scan-repository command.
*/
static execCreateFixPullRequests() {
return __awaiter(this, void 0, void 0, function* () {
if (!process.env.JF_GIT_BASE_BRANCH) {
// Get the current branch we are checked on
const git = (0, simple_git_1.simpleGit)();
try {
const currentBranch = yield git.branch();
core.exportVariable('JF_GIT_BASE_BRANCH', currentBranch.current);
}
catch (error) {
throw new Error('Error getting current branch from the .git folder: ' + error);
}
}
let res = yield (0, exec_1.exec)(Utils.getExecutableName(), ['scan-repository']);
if (res !== core.ExitCode.Success) {
throw new Error('Frogbot exited with exit code ' + res);
}
});
}
/**
* Try to load the Frogbot executables from cache.
*
* @param version - Frogbot version
* @returns true if the CLI executable was loaded from cache and added to path
*/
static loadFromCache(version) {
let execPath = (0, tool_cache_1.find)(Utils.TOOL_NAME, version);
if (execPath) {
core.addPath(execPath);
return true;
}
return false;
}
/**
* Add Frogbot executable to cache and to the system path.
* @param downloadDir - The directory whereby the CLI was downloaded to
* @param version - Frogbot version
* @param fileName - 'frogbot' or 'frogbot.exe'
*/
static cacheAndAddPath(downloadDir, version, fileName) {
return __awaiter(this, void 0, void 0, function* () {
let cliDir = yield (0, tool_cache_1.cacheFile)(downloadDir, fileName, Utils.TOOL_NAME, version);
if (!Utils.isWindows()) {
let filePath = (0, path_1.normalize)((0, path_1.join)(cliDir, fileName));
(0, fs_1.chmodSync)(filePath, 0o555);
}
core.addPath(cliDir);
});
}
static getCliUrl(major, version, fileName, releasesRepo) {
var _a;
let architecture = 'frogbot-' + Utils.getArchitecture();
if (releasesRepo) {
let platformUrl = (_a = process.env.JF_URL) !== null && _a !== void 0 ? _a : '';
if (!platformUrl) {
throw new Error('Failed while downloading Frogbot from Artifactory, JF_URL must be set');
}
// Remove trailing slash if exists
platformUrl = platformUrl.replace(/\/$/, '');
return `${platformUrl}/artifactory/${releasesRepo}/artifactory/frogbot/v${major}/${version}/${architecture}/${fileName}`;
}
return `https://releases.jfrog.io/artifactory/frogbot/v${major}/${version}/${architecture}/${fileName}`;
}
static getArchitecture() {
if (Utils.isWindows()) {
return 'windows-amd64';
}
if ((0, os_1.platform)().includes('darwin')) {
if ((0, os_1.arch)().includes('arm')) {
return 'mac-arm64';
}
return 'mac-386';
}
if ((0, os_1.arch)().includes('arm')) {
return (0, os_1.arch)().includes('64') ? 'linux-arm64' : 'linux-arm';
}
if ((0, os_1.arch)().includes('ppc64le')) {
return 'linux-ppc64le';
}
if ((0, os_1.arch)().includes('ppc64')) {
return 'linux-ppc64';
}
return (0, os_1.arch)().includes('64') ? 'linux-amd64' : 'linux-386';
}
static getExecutableName() {
return Utils.isWindows() ? 'frogbot.exe' : 'frogbot';
}
static isWindows() {
return (0, os_1.platform)().startsWith('win');
}
static getJfrogPlatformUrl() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let jfrogUrl = (_a = process.env.JF_URL) !== null && _a !== void 0 ? _a : '';
if (!jfrogUrl) {
throw new Error('JF_URL must be provided and point on your full platform URL, for example: https://mycompany.jfrog.io/');
}
return jfrogUrl;
});
}
/**
* This method will set up an OIDC token if the OIDC integration is set.
* If OIDC integration is set but not working, the action will fail causing frogbot to fail
* @param jfrogUrl - The JFrog platform URL
*/
static setupOidcTokenIfNeeded(jfrogUrl) {
return __awaiter(this, void 0, void 0, function* () {
const oidcProviderName = core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME_ARG);
if (!oidcProviderName) {
// No token is set if an oidc-provider-name wasn't provided
return;
}
core.debug('Obtaining an access token through OpenID Connect...');
const audience = core.getInput(Utils.OIDC_AUDIENCE_ARG);
let jsonWebToken;
try {
core.debug('Fetching JSON web token');
jsonWebToken = yield core.getIDToken(audience);
}
catch (error) {
throw new Error(`Getting openID Connect JSON web token failed: ${error.message}`);
}
try {
return yield this.initJfrogAccessTokenThroughOidcProtocol(jfrogUrl, jsonWebToken, oidcProviderName);
}
catch (error) {
throw new Error(`OIDC authentication against JFrog platform failed, please check OIDC settings and mappings on the JFrog platform: ${error.message}`);
}
});
}
/**
* This method exchanges a JSON web token with a JFrog access token through the OpenID Connect protocol
* If we've reached this stage, the jfrogUrl field should hold a non-empty value obtained from process.env.JF_URL
* @param jfrogUrl - The JFrog platform URL
* @param jsonWebToken - The JSON web token used in the token exchange
* @param oidcProviderName - The OpenID Connect provider name
*/
static initJfrogAccessTokenThroughOidcProtocol(jfrogUrl, jsonWebToken, oidcProviderName) {
return __awaiter(this, void 0, void 0, function* () {
const exchangeUrl = jfrogUrl.replace(/\/$/, '') + '/access/api/v1/oidc/token';
core.debug('Exchanging GitHub JSON web token with a JFrog access token...');
const httpClient = new http_client_1.HttpClient();
const data = `{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤫 Secret Vulnerability

Severity Origin Finding
high
High
jfrog Hardcoded secrets were found
Full description

Vulnerability Details

Abbreviation: REQ.SECRET.GENERIC.CODE

Storing hardcoded secrets in your source code or binary artifact could lead to several risks.

If the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.

Best practices

Use safe storage when storing high-privilege secrets such as passwords and tokens, for example -

  • Environment Variables

Environment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -
SECRET_VAR=MySecret ./my_application
This way, MySecret does not have to be hardcoded into my_application.

Note that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the Dockerfile or one of the container's files) but rather must be passed dynamically, for example in the docker run call as an argument.

  • Secret management services

External vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -

Least-privilege principle

Storing a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.
For example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation only (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.
That being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.



"subject_token": "${jsonWebToken}",
"provider_name": "${oidcProviderName}"
}`;
const additionalHeaders = {
'Content-Type': 'application/json',
};
const response = yield httpClient.post(exchangeUrl, data, additionalHeaders);
const responseString = yield response.readBody();
const statusCode = response.message.statusCode;
if (!statusCode || statusCode < 200 || statusCode >= 300) {
throw new Error(`Token exchange failed with HTTP ${statusCode}: ${responseString}`);
}
const responseJson = JSON.parse(responseString);
if (responseJson.errors) {
throw new Error(`${JSON.stringify(responseJson.errors)}`);
}
if (responseJson.error) {
throw new Error(`${responseJson.error}${responseJson.error_description ? ': ' + responseJson.error_description : ''}`);
}
if (!responseJson.access_token) {
throw new Error(`Token exchange response is missing access_token. Full response: ${responseString}`);
}
core.setSecret(responseJson.access_token);
process.env.JF_ACCESS_TOKEN = responseJson.access_token;
});
}
}
exports.Utils = Utils;
Utils.LATEST_RELEASE_VERSION = '[RELEASE]';
Utils.LATEST_CLI_VERSION_ARG = 'latest';
Utils.VERSION_ARG = 'version';
Utils.TOOL_NAME = 'frogbot';
// OpenID Connect audience input
Utils.OIDC_AUDIENCE_ARG = 'oidc-audience';
// OpenID Connect provider_name input
Utils.OIDC_INTEGRATION_PROVIDER_NAME_ARG = 'oidc-provider-name';
Loading
Loading