diff --git a/esp/services/ws_workunits/ws_workunitsHelpers.cpp b/esp/services/ws_workunits/ws_workunitsHelpers.cpp index 1a2de1ce067..ae6dc3a1da3 100644 --- a/esp/services/ws_workunits/ws_workunitsHelpers.cpp +++ b/esp/services/ws_workunits/ws_workunitsHelpers.cpp @@ -4469,7 +4469,11 @@ void CWsWuFileHelper::zipZAPFiles(const char* parentFolder, const char* zapFiles else zipCommand.setf("cd %s\nzip -r", parentFolder); if (!isEmptyString(passwordReq)) - zipCommand.append(" --password ").append(passwordReq); + { + StringBuffer sanitizedPassword; + sanitizeCommandArg(passwordReq, sanitizedPassword); + zipCommand.append(" --password ").append(sanitizedPassword); + } zipCommand.append(" ").append(zipFileNameWithFullPath).append(" ").append(zapFiles); int zipRet = system(zipCommand); if (zipRet != 0) diff --git a/esp/src/src-react/components/Logs.tsx b/esp/src/src-react/components/Logs.tsx index c02f3388e5f..2091699f373 100644 --- a/esp/src/src-react/components/Logs.tsx +++ b/esp/src/src-react/components/Logs.tsx @@ -113,7 +113,7 @@ export const Logs: React.FunctionComponent = ({ const now = React.useMemo(() => new Date(), []); - const { columns: logColumns } = useLogAccessInfo(); + const { logsColumns: logColumns } = useLogAccessInfo(); // Grid --- const columns = React.useMemo((): FluentColumns => { diff --git a/esp/src/src-react/components/Menu.tsx b/esp/src/src-react/components/Menu.tsx index d2c1e3da292..f9bc7b5a5e3 100644 --- a/esp/src/src-react/components/Menu.tsx +++ b/esp/src/src-react/components/Menu.tsx @@ -2,11 +2,11 @@ import * as React from "react"; import { IconButton, IContextualMenuItem, INavLink, INavLinkGroup, Link, mergeStyleSets, Nav, Stack } from "@fluentui/react"; import { useConst } from "@fluentui/react-hooks"; import nlsHPCC from "src/nlsHPCC"; -import { hasLogAccess } from "src/ESPLog"; import { containerized, bare_metal } from "src/BuildInfo"; import { navCategory } from "../util/history"; import { MainNav, routes } from "../routes"; import { useFavorite, useFavorites, useHistory } from "../hooks/favorite"; +import { useLogAccessInfo } from "../hooks/platform"; import { useSessionStore } from "../hooks/store"; import { useUserTheme } from "../hooks/theme"; import { useMyAccount } from "../hooks/user"; @@ -295,12 +295,7 @@ export const SubNavigation: React.FunctionComponent = ({ } }), [theme]); - const [logsDisabled, setLogsDisabled] = React.useState(true); - React.useEffect(() => { - hasLogAccess().then(response => { - setLogsDisabled(!response); - }); - }, []); + const { logsEnabled, logsStatusMessage } = useLogAccessInfo(); const linkStyle = React.useCallback((disabled) => { return disabled ? { background: themeV9.colorNeutralBackgroundDisabled, @@ -326,9 +321,10 @@ export const SubNavigation: React.FunctionComponent = ({ {subMenuItems[mainNav]?.map((row, idx) => { - const linkDisabled = (row.itemKey === "/topology/logs" && logsDisabled) || (row.itemKey.indexOf("security") > -1 && !isAdmin); + const linkDisabled = (row.itemKey === "/topology/logs" && !logsEnabled) || (row.itemKey.indexOf("security") > -1 && !isAdmin); return = ({ const [variables, , , refreshVariables] = useWorkunitVariables(wuid); const [otTraceParent, setOtTraceParent] = React.useState(""); const [logCount, setLogCount] = React.useState("*"); - const [logsDisabled, setLogsDisabled] = React.useState(true); + const { logsEnabled, logsStatusMessage } = useLogAccessInfo(); const [_nextPrev, setNextPrev] = useNextPrev(); const query = React.useMemo(() => { @@ -121,13 +120,6 @@ export const WorkunitDetails: React.FunctionComponent = ({ }; }, [nextWuid, query, setNextPrev, wuid]); - useDeepEffect(() => { - hasLogAccess().then(response => { - setLogsDisabled(!response); - return response; - }); - }, [wuid], [queryParams]); - const onTabSelect = React.useCallback((tab: TabInfo) => { pushUrl(tab.__state ?? `${parentUrl}/${wuid}/${tab.id}`); updateFullscreen(fullscreen); @@ -174,7 +166,8 @@ export const WorkunitDetails: React.FunctionComponent = ({ id: "logs", label: nlsHPCC.Logs, count: logCount, - disabled: logsDisabled + tooltipText: !logsEnabled ? (logsStatusMessage || nlsHPCC.LogsDisabled) : null, + disabled: !logsEnabled }, { id: "eclsummary", label: nlsHPCC.ECL @@ -182,7 +175,7 @@ export const WorkunitDetails: React.FunctionComponent = ({ id: "xml", label: nlsHPCC.XML }]; - }, [logCount, logsDisabled, workunit?.ApplicationValueCount, workunit?.DebugValueCount, workunit?.GraphCount, workunit?.HelpersCount, workunit?.ResourceURLCount, workunit?.ResultCount, workunit?.SourceFileCount, workunit?.VariableCount, workunit?.WorkflowCount, wuid]); + }, [logCount, logsEnabled, logsStatusMessage, workunit?.ApplicationValueCount, workunit?.DebugValueCount, workunit?.GraphCount, workunit?.HelpersCount, workunit?.ResourceURLCount, workunit?.ResultCount, workunit?.SourceFileCount, workunit?.VariableCount, workunit?.WorkflowCount, wuid]); return {({ size }) => diff --git a/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx b/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx index e9cf3520ccf..63a15308e08 100644 --- a/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx +++ b/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { Overflow, OverflowItem, SelectTabData, SelectTabEvent, Tab, TabList } from "@fluentui/react-components"; +import { Overflow, OverflowItem, SelectTabData, SelectTabEvent, Tab, TabList, Tooltip } from "@fluentui/react-components"; import { Count } from "./Count"; import { TabInfo } from "./TabInfo"; import { OverflowMenu } from "../OverflowMenu"; @@ -28,7 +28,9 @@ export const OverflowTabList: React.FunctionComponent = ({ tab.__state = state; } return - {tab.label} + + {tab.label} + ; }), tabsIndex]; }, [selected, state, tabs]); diff --git a/esp/src/src-react/components/controls/TabbedPanes/TabInfo.ts b/esp/src/src-react/components/controls/TabbedPanes/TabInfo.ts index 7d87929b3a6..d21edad8120 100644 --- a/esp/src/src-react/components/controls/TabbedPanes/TabInfo.ts +++ b/esp/src/src-react/components/controls/TabbedPanes/TabInfo.ts @@ -6,5 +6,6 @@ export interface TabInfo { label: string; count?: string | number; disabled?: boolean; + tooltipText?: string; __state?: any; } diff --git a/esp/src/src-react/hooks/platform.ts b/esp/src/src-react/hooks/platform.ts index 33f2089c260..e403a37c119 100644 --- a/esp/src/src-react/hooks/platform.ts +++ b/esp/src/src-react/hooks/platform.ts @@ -3,6 +3,7 @@ import { Octokit } from "octokit"; import { useConst } from "@fluentui/react-hooks"; import { scopedLogger } from "@hpcc-js/util"; import { LogaccessService, Topology, WsLogaccess, WsTopology, WorkunitsServiceEx } from "@hpcc-js/comms"; +import nlsHPCC from "src/nlsHPCC"; import { getBuildInfo, BuildInfo, fetchModernMode } from "src/Session"; import { cmake_build_type, containerized, ModernMode } from "src/BuildInfo"; import { sessionKeyValStore, userKeyValStore } from "src/KeyValStore"; @@ -209,19 +210,34 @@ export function useModernMode(): { return { modernMode, setModernMode }; } -export function useLogAccessInfo(): { - managerType: string; - columns: WsLogaccess.Column[] -} { - const [managerType, setManagerType] = React.useState(""); - const [columns, setColumns] = React.useState(); +interface LogAccessInfo { + logsEnabled: boolean; + logsManagerType: string; + logsColumns: WsLogaccess.Column[]; + logsStatusMessage: string; +} + +export function useLogAccessInfo(): LogAccessInfo { + const [logsEnabled, setLogsEnabled] = React.useState(false); + const [logsManagerType, setLogsManagerType] = React.useState(""); + const [logsColumns, setLogsColumns] = React.useState(); + const [logsStatusMessage, setLogsStatusMessage] = React.useState(""); React.useEffect(() => { service.GetLogAccessInfo({}).then(response => { - setManagerType(response.RemoteLogManagerType ?? ""); - setColumns(response?.Columns?.Column); + if (response.hasOwnProperty("Exceptions")) { + setLogsStatusMessage(response["Exceptions"]?.Exception[0]?.Message ?? nlsHPCC.LogAccess_GenericException); + } else { + if (response.RemoteLogManagerType === null) { + setLogsStatusMessage(nlsHPCC.LogAccess_LoggingNotConfigured); + } else { + setLogsEnabled(true); + setLogsManagerType(response.RemoteLogManagerType); + setLogsColumns(response?.Columns?.Column); + } + } }); }, []); - return { managerType, columns }; + return { logsEnabled, logsManagerType, logsColumns, logsStatusMessage }; } \ No newline at end of file diff --git a/esp/src/src/nls/hpcc.ts b/esp/src/src/nls/hpcc.ts index 592e8cb09d3..182faf5b59f 100644 --- a/esp/src/src/nls/hpcc.ts +++ b/esp/src/src/nls/hpcc.ts @@ -496,6 +496,8 @@ export = { Location: "Location", Lock: "Lock", LogAccessType: "Log Access Type", + LogAccess_GenericException: "ws_logaccess::GetLogAccessInfo, an exception has occurred.", + LogAccess_LoggingNotConfigured: "A logging engine has not been configured.", LogDirectory: "Log Directory", LogEventType: "Log Event Type", LogFile: "Log File", @@ -529,6 +531,7 @@ export = { Login: "Login", Logout: "Log Out", Logs: "Logs", + LogsDisabled: "Logs Disabled", LogVisualization: "Log Visualization", LogVisualizationUnconfigured: "Log Visualization is not configured, please check your configuration manager settings.", log_analysis_1: "log_analysis_1*", diff --git a/system/jlib/jstring.cpp b/system/jlib/jstring.cpp index 30c9c5fc078..55430778585 100644 --- a/system/jlib/jstring.cpp +++ b/system/jlib/jstring.cpp @@ -2925,3 +2925,32 @@ const char * stristr (const char *haystack, const char *needle) return nullptr; } + +/** + * For preventing command injection, sanitize the argument to be passed to the system command. + * - Quote the entire argument with single quotes to prevent interpretation of shell metacharacters. + * - Since a single-quoted string can't contain single quotes, even escaped, replace each single + * quote in the argument with the sequence '"'"' . That closes the single quoted string, appends + * a literal single quote, and reopens the single quoted string + */ +StringBuffer& sanitizeCommandArg(const char* arg, StringBuffer& sanitized) +{ +#if defined(__linux__) || defined(__APPLE__) + if (!isEmptyString(arg)) + { + size_t len = strlen(arg); + sanitized.append('\''); + for (size_t i = 0; i < len; i++) + { + if (arg[i] == '\'') + sanitized.append(R"('"'"')"); + else + sanitized.append(arg[i]); + } + sanitized.append('\''); + } +#else + sanitized.append(arg); +#endif + return sanitized; +} diff --git a/system/jlib/jstring.hpp b/system/jlib/jstring.hpp index 5a153555041..3b84b727738 100644 --- a/system/jlib/jstring.hpp +++ b/system/jlib/jstring.hpp @@ -642,4 +642,7 @@ extern jlib_decl void processOptionString(const char * options, optionCallback c extern jlib_decl const char * stristr(const char *haystack, const char *needle); extern jlib_decl void getSnakeCase(StringBuffer & out, const char * camelValue); +// For preventing command injection, sanitize the argument to be passed to the system command +extern jlib_decl StringBuffer& sanitizeCommandArg(const char* arg, StringBuffer& sanitized); + #endif