-
Notifications
You must be signed in to change notification settings - Fork 107
Fix frogbot security issues v3 #1409
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
Draft
eranturgeman
wants to merge
7
commits into
jfrog:main
Choose a base branch
from
eranturgeman:fix-frogbot-security-issues-v3
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
72510d2
comitting fixes to security issues
eranturgeman 8336bd0
comitting fixes to security issues
eranturgeman f9d04ac
comitting fixes to security issues
eranturgeman cc1677e
Update action/node_modules/.package-lock.json
eranturgeman 1fb7e48
fix for unreal secret
eranturgeman 0e89cc6
Merge branch 'main' of https://github.com/jfrog/frogbot into fix-frog…
eranturgeman eee6b17
Merge branch 'main' of https://github.com/jfrog/frogbot into fix-frog…
eranturgeman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| "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'; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤫 Secret Vulnerability
High
Full description
Vulnerability Details
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_applicationThis way,
MySecretdoes not have to be hardcoded intomy_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
Dockerfileor one of the container's files) but rather must be passed dynamically, for example in thedocker runcall 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.
🐸 JFrog Frogbot