feat: added masking of secrets in cli in url

This commit is contained in:
Daniel Maurer
2023-07-25 11:04:06 +02:00
committed by nivedin
parent 088f1d6b47
commit e0eb8af6f5
11 changed files with 88 additions and 34 deletions

View File

@@ -14,9 +14,10 @@ import { isHoppCLIError } from "../utils/checks";
export const test = (path: string, options: TestCmdOptions) => async () => { export const test = (path: string, options: TestCmdOptions) => async () => {
try { try {
const delay = options.delay ? parseDelayOption(options.delay) : 0 const delay = options.delay ? parseDelayOption(options.delay) : 0
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] } const envName = options.envName
const envs = options.env ? await parseEnvsData(options.env, envName) : <HoppEnvs>{ global: [], selected: [] }
const collections = await parseCollectionData(path) const collections = await parseCollectionData(path)
const report = await collectionsRunner({collections, envs, delay}) const report = await collectionsRunner({collections, envs, delay})
const hasSucceeded = collectionsRunnerResult(report) const hasSucceeded = collectionsRunnerResult(report)
collectionsRunnerExit(hasSucceeded) collectionsRunnerExit(hasSucceeded)

View File

@@ -51,6 +51,9 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
case "MALFORMED_COLLECTION": case "MALFORMED_COLLECTION":
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`; ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
break; break;
case "ENVIRONMENT_NAME_NOT_FOUND":
ERROR_MSG = `\n${parseErrorData(error.data)}`;
break;
case "NO_FILE_PATH": case "NO_FILE_PATH":
ERROR_MSG = `Please provide a hoppscotch-collection file path.`; ERROR_MSG = `Please provide a hoppscotch-collection file path.`;
break; break;

View File

@@ -50,6 +50,7 @@ program
"path to a hoppscotch collection.json file for CI testing" "path to a hoppscotch collection.json file for CI testing"
) )
.option("-e, --env <file_path>", "path to an environment variables json file") .option("-e, --env <file_path>", "path to an environment variables json file")
.option("-eN, --envName <environment_name>","Specific Name of the environment")
.option( .option(
"-d, --delay <delay_in_ms>", "-d, --delay <delay_in_ms>",
"delay in milliseconds(ms) between consecutive requests within a collection" "delay in milliseconds(ms) between consecutive requests within a collection"

View File

@@ -33,4 +33,5 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string; active: boolean }[]; effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
effectiveFinalParams: { key: string; value: string; active: boolean }[]; effectiveFinalParams: { key: string; value: string; active: boolean }[];
effectiveFinalBody: FormData | string | null; effectiveFinalBody: FormData | string | null;
effectiveFinalMaskedURL: string;
} }

View File

@@ -5,23 +5,46 @@ import { readJsonFile } from "../../utils/mutators";
/** /**
* Parses env json file for given path and validates the parsed env json object. * Parses env json file for given path and validates the parsed env json object.
* @param path Path of env.json file to be parsed. * @param path Path of env.json file to be parsed.
* @param envName Name of the environment that should be used. If undefined first environment is used.
* @returns For successful parsing we get HoppEnvs object. * @returns For successful parsing we get HoppEnvs object.
*/ */
export async function parseEnvsData(path: string) { export async function parseEnvsData(path: string, envName: string | undefined) {
const contents = await readJsonFile(path) const contents = await readJsonFile(path)
if(!(contents && typeof contents === "object" && Array.isArray(contents))) {
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: null }) throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
} }
const envPairs: Array<HoppEnvPair> = [] const envPairs: Array<HoppEnvPair> = []
for( const [key,value] of Object.entries(contents)) { const contentEntries = Object.entries(contents)
if(typeof value !== "string") { let environmentFound = false;
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
}
envPairs.push({key, value}) for(const [key, obj] of contentEntries) {
if(!(typeof obj === "object" && "name" in obj && "variables" in obj)) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: obj } })
}
if(envName && envName !== obj.name) {
continue
}
environmentFound = true;
for(const variable of obj.variables) {
if(
!(
typeof variable === "object" &&
"key" in variable &&
"value" in variable &&
"secret" in variable
)
) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: variable } });
}
const { key, value, secret } = variable;
envPairs.push({ key: key, value: value, secret: secret });
}
break
}
if(envName && !environmentFound) {
throw error({ code: "ENVIRONMENT_NAME_NOT_FOUND", data: envName });
} }
return <HoppEnvs>{ global: [], selected: envPairs } return <HoppEnvs>{ global: [], selected: envPairs }
} }

View File

@@ -1,6 +1,7 @@
export type TestCmdOptions = { export type TestCmdOptions = {
env: string | undefined; env: string | undefined;
delay: string | undefined; delay: string | undefined;
envName: string | undefined;
}; };
export type HOPP_ENV_FILE_EXT = "json"; export type HOPP_ENV_FILE_EXT = "json";

View File

@@ -15,6 +15,7 @@ type HoppErrors = {
FILE_NOT_FOUND: HoppErrorPath; FILE_NOT_FOUND: HoppErrorPath;
UNKNOWN_COMMAND: HoppErrorCmd; UNKNOWN_COMMAND: HoppErrorCmd;
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData; MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
ENVIRONMENT_NAME_NOT_FOUND: HoppErrorData;
NO_FILE_PATH: {}; NO_FILE_PATH: {};
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData; PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
PARSING_ERROR: HoppErrorData; PARSING_ERROR: HoppErrorData;

View File

@@ -1,7 +1,7 @@
import { bold } from "chalk"; import { bold } from "chalk";
import { groupEnd, group, log } from "console"; import { groupEnd, group, log } from "console";
import { handleError } from "../handlers/error"; import { handleError } from "../handlers/error";
import { RequestConfig } from "../interfaces/request"; import { Method } from "axios";
import { RequestRunnerResponse, TestReport } from "../interfaces/response"; import { RequestRunnerResponse, TestReport } from "../interfaces/response";
import { HoppCLIError } from "../types/errors"; import { HoppCLIError } from "../types/errors";
import { import {
@@ -172,11 +172,12 @@ export const printFailedTestsReport = (
export const printRequestRunner = { export const printRequestRunner = {
/** /**
* Request-runner starting message. * Request-runner starting message.
* @param requestConfig Provides request's method and url. * @param requestMethod Provides request's method
* @param maskedURL Provides the URL with secrets masked with asterisks
*/ */
start: (requestConfig: RequestConfig) => { start: (requestMethod: Method | undefined, maskedURL: string) => {
const METHOD = BG_INFO(` ${requestConfig.method} `); const METHOD = BG_INFO(` ${requestMethod} `);
const ENDPOINT = requestConfig.url; const ENDPOINT = maskedURL;
process.stdout.write(`${METHOD} ${ENDPOINT}`); process.stdout.write(`${METHOD} ${ENDPOINT}`);
}, },

View File

@@ -50,9 +50,9 @@ export const preRequestScriptRunner = (
isHoppCLIError(reason) isHoppCLIError(reason)
? reason ? reason
: error({ : error({
code: "PRE_REQUEST_SCRIPT_ERROR", code: "PRE_REQUEST_SCRIPT_ERROR",
data: reason, data: reason,
}) })
) )
); );
@@ -151,6 +151,12 @@ export function getEffectiveRESTRequest(
request.endpoint, request.endpoint,
envVariables envVariables
); );
const maskedEnvVariables = setAllEnvironmentValuesToAsterisk(envVariables)
const _effectiveFinalMaskedURL = parseTemplateStringE(
request.endpoint,
maskedEnvVariables)
if (E.isLeft(_effectiveFinalURL)) { if (E.isLeft(_effectiveFinalURL)) {
return E.left( return E.left(
error({ error({
@@ -160,6 +166,7 @@ export function getEffectiveRESTRequest(
); );
} }
const effectiveFinalURL = _effectiveFinalURL.right; const effectiveFinalURL = _effectiveFinalURL.right;
const effectiveFinalMaskedURL = E.isLeft(_effectiveFinalMaskedURL) ? request.endpoint : _effectiveFinalMaskedURL.right;
return E.right({ return E.right({
...request, ...request,
@@ -167,6 +174,7 @@ export function getEffectiveRESTRequest(
effectiveFinalHeaders, effectiveFinalHeaders,
effectiveFinalParams, effectiveFinalParams,
effectiveFinalBody, effectiveFinalBody,
effectiveFinalMaskedURL,
}); });
} }
@@ -242,15 +250,15 @@ function getFinalBodyFromRequest(
arrayFlatMap((x) => arrayFlatMap((x) =>
x.isFile x.isFile
? x.value.map((v) => ({ ? x.value.map((v) => ({
key: parseTemplateString(x.key, envVariables), key: parseTemplateString(x.key, envVariables),
value: v as string | Blob, value: v as string | Blob,
})) }))
: [ : [
{ {
key: parseTemplateString(x.key, envVariables), key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables), value: parseTemplateString(x.value, envVariables),
}, },
] ]
), ),
toFormData, toFormData,
E.right E.right
@@ -287,3 +295,22 @@ export const getPreRequestMetrics = (
hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 }, hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(scripts) => <PreRequestMetrics>{ scripts, duration } (scripts) => <PreRequestMetrics>{ scripts, duration }
); );
/**
* Mask all environment values with asterisks
* @param variables Environment variable array
* @returns Environment variable array with masked values
*/
const setAllEnvironmentValuesToAsterisk = (
variables: Environment["variables"]
): Environment["variables"] => {
const envVariables: Environment["variables"] = [];
for (const variable of variables) {
let value = variable.value
if (variable.secret) {
value = "******"
}
envVariables.push({ key: variable.key, secret: variable.secret, value: value })
}
return envVariables
}

View File

@@ -220,6 +220,7 @@ export const processRequest =
effectiveFinalHeaders: [], effectiveFinalHeaders: [],
effectiveFinalParams: [], effectiveFinalParams: [],
effectiveFinalURL: "", effectiveFinalURL: "",
effectiveFinalMaskedURL:"",
}; };
// Executing pre-request-script // Executing pre-request-script
@@ -237,8 +238,8 @@ export const processRequest =
// Creating request-config for request-runner. // Creating request-config for request-runner.
const requestConfig = createRequest(effectiveRequest); const requestConfig = createRequest(effectiveRequest);
printRequestRunner.start(requestConfig); printRequestRunner.start(requestConfig.method, effectiveRequest.effectiveFinalMaskedURL);
// Default value for request-runner's response. // Default value for request-runner's response.
let _requestRunnerRes: RequestRunnerResponse = { let _requestRunnerRes: RequestRunnerResponse = {

View File

@@ -95,22 +95,16 @@ declare module 'vue' {
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver'] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'] HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'] HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow'] HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows'] HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default'] HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']