import React, { useState, useContext, useEffect, KeyboardEvent } from "react"; import { InjectedRouter } from "react-router"; import { size } from "lodash"; import classnames from "classnames"; import { useDebouncedCallback } from "use-debounce"; import PATHS from "router/paths"; import { AppContext } from "context/app"; import { QueryContext } from "context/query"; import { NotificationContext } from "context/notification"; import { addGravatarUrlToResource } from "utilities/helpers"; import usePlatformCompatibility from "hooks/usePlatformCompatibility"; import { IApiError } from "interfaces/errors"; import { IQuery, IQueryFormData } from "interfaces/query"; import queryAPI from "services/entities/queries"; import { IAceEditor } from "react-ace/lib/types"; import ReactTooltip from "react-tooltip"; import Avatar from "components/Avatar"; import FleetAce from "components/FleetAce"; // @ts-ignore import validateQuery from "components/forms/validators/validate_query"; import Button from "components/buttons/Button"; import RevealButton from "components/buttons/RevealButton"; import Checkbox from "components/forms/fields/Checkbox"; import Spinner from "components/Spinner"; import AutoSizeInputField from "components/forms/fields/AutoSizeInputField"; import NewQueryModal from "../NewQueryModal"; import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png"; import PencilIcon from "../../../../../../assets/images/icon-pencil-14x14@2x.png"; const baseClass = "query-form"; interface IQueryFormProps { router: InjectedRouter; queryIdForEdit: number | null; showOpenSchemaActionText: boolean; storedQuery: IQuery | undefined; isStoredQueryLoading: boolean; isQuerySaving: boolean; isQueryUpdating: boolean; onCreateQuery: (formData: IQueryFormData) => void; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; onUpdate: (formData: IQueryFormData) => void; onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; backendValidators: { [key: string]: string }; } const validateQuerySQL = (query: string) => { const errors: { [key: string]: string } = {}; const { error: queryError, valid: queryValid } = validateQuery(query); if (!queryValid) { errors.query = queryError; } const valid = !size(errors); return { valid, errors }; }; const QueryForm = ({ router, queryIdForEdit, showOpenSchemaActionText, storedQuery, isStoredQueryLoading, isQuerySaving, isQueryUpdating, onCreateQuery, onOsqueryTableSelect, goToSelectTargets, onUpdate, onOpenSchemaSidebar, renderLiveQueryWarning, backendValidators, }: IQueryFormProps): JSX.Element => { const isEditMode = !!queryIdForEdit; const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); const [showQueryEditor, setShowQueryEditor] = useState(false); const [isEditingName, setIsEditingName] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false); const [isSaveAsNewLoading, setIsSaveAsNewLoading] = useState(false); // Note: The QueryContext values should always be used for any mutable query data such as query name // The storedQuery prop should only be used to access immutable metadata such as author id const { lastEditedQueryId, lastEditedQueryName, lastEditedQueryDescription, lastEditedQueryBody, lastEditedQueryObserverCanRun, setLastEditedQueryName, setLastEditedQueryDescription, setLastEditedQueryBody, setLastEditedQueryObserverCanRun, } = useContext(QueryContext); const { currentUser, isOnlyObserver, isGlobalObserver, isAnyTeamMaintainerOrTeamAdmin, isGlobalAdmin, isGlobalMaintainer, } = useContext(AppContext); const { renderFlash } = useContext(NotificationContext); const platformCompatibility = usePlatformCompatibility(); const { setCompatiblePlatforms } = platformCompatibility; const debounceSQL = useDebouncedCallback((sql: string) => { let valid = true; const { valid: isValidated, errors: newErrors } = validateQuerySQL(sql); valid = isValidated; setErrors({ ...newErrors, }); }, 500); queryIdForEdit = queryIdForEdit || 0; useEffect(() => { if (!isStoredQueryLoading && queryIdForEdit === lastEditedQueryId) { setCompatiblePlatforms(lastEditedQueryBody); } debounceSQL(lastEditedQueryBody); }, [lastEditedQueryBody, lastEditedQueryId]); const hasTeamMaintainerPermissions = isEditMode ? isAnyTeamMaintainerOrTeamAdmin && storedQuery && currentUser && storedQuery.author_id === currentUser.id : isAnyTeamMaintainerOrTeamAdmin; const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer; const onLoad = (editor: IAceEditor) => { editor.setOptions({ enableLinking: true, }); // @ts-expect-error // the string "linkClick" is not officially in the lib but we need it editor.on("linkClick", (data: EditorSession) => { const { type, value } = data.token; if (type === "osquery-token") { return onOsqueryTableSelect(value); } return false; }); }; const onChangeQuery = (sqlString: string) => { setLastEditedQueryBody(sqlString); }; const onInputKeypress = (event: KeyboardEvent) => { if (event.key.toLowerCase() === "enter" && !event.shiftKey) { event.preventDefault(); event.currentTarget.blur(); setIsEditingName(false); setIsEditingDescription(false); } }; const promptSaveAsNewQuery = () => ( evt: React.MouseEvent ) => { evt.preventDefault(); if (isEditMode && !lastEditedQueryName) { return setErrors({ ...errors, name: "Query name must be present", }); } let valid = true; const { valid: isValidated } = validateQuerySQL(lastEditedQueryBody); valid = isValidated; if (valid) { setIsSaveAsNewLoading(true); queryAPI .create({ name: lastEditedQueryName, description: lastEditedQueryDescription, query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, }) .then((response: { query: IQuery }) => { setIsSaveAsNewLoading(false); router.push(PATHS.EDIT_QUERY(response.query)); renderFlash("success", `Successfully added query.`); }) .catch((createError: { data: IApiError }) => { if (createError.data.errors[0].reason.includes("already exists")) { queryAPI .create({ name: `Copy of ${lastEditedQueryName}`, description: lastEditedQueryDescription, query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, }) .then((response: { query: IQuery }) => { setIsSaveAsNewLoading(false); router.push(PATHS.EDIT_QUERY(response.query)); renderFlash( "success", `Successfully added query as "Copy of ${lastEditedQueryName}".` ); }) .catch((createCopyError: { data: IApiError }) => { if ( createCopyError.data.errors[0].reason.includes( "already exists" ) ) { renderFlash( "error", `"Copy of ${lastEditedQueryName}" already exists. Please rename your query and try again.` ); } setIsSaveAsNewLoading(false); }); } else { setIsSaveAsNewLoading(false); renderFlash("error", "Could not create query. Please try again."); } }); } }; const promptSaveQuery = () => (evt: React.MouseEvent) => { evt.preventDefault(); if (isEditMode && !lastEditedQueryName) { return setErrors({ ...errors, name: "Query name must be present", }); } let valid = true; const { valid: isValidated } = validateQuerySQL(lastEditedQueryBody); valid = isValidated; if (valid) { if (!isEditMode) { setIsSaveModalOpen(true); } else { onUpdate({ name: lastEditedQueryName, description: lastEditedQueryDescription, query: lastEditedQueryBody, observer_can_run: lastEditedQueryObserverCanRun, }); } } }; const renderAuthor = (): JSX.Element | null => { return storedQuery ? ( <> Author
{storedQuery.author_name === currentUser?.name ? "You" : storedQuery.author_name}
) : null; }; const renderLabelComponent = (): JSX.Element | null => { if (!showOpenSchemaActionText) { return null; } return ( ); }; const renderPlatformCompatibility = () => { if (isStoredQueryLoading || queryIdForEdit !== lastEditedQueryId) { return null; } return platformCompatibility.render(); }; const queryNameClasses = classnames("query-name-wrapper", { [`${baseClass}--editing`]: isEditingName, }); const queryDescriptionClasses = classnames("query-description-wrapper", { [`${baseClass}--editing`]: isEditingDescription, }); const renderName = () => { if (isEditMode) { return ( <>
setIsEditingName(true)} onBlur={() => setIsEditingName(false)} onKeyPress={onInputKeypress} isFocused={isEditingName} /> setIsEditingName(true)}> Edit name
); } return

New query

; }; const renderDescription = () => { if (isEditMode) { return ( <>
setIsEditingDescription(true)} onBlur={() => setIsEditingDescription(false)} onKeyPress={onInputKeypress} isFocused={isEditingDescription} /> setIsEditingDescription(true)} > Edit name
); } return null; }; const renderRunForObserver = (

{lastEditedQueryName}

{lastEditedQueryDescription}

{renderAuthor()}
setShowQueryEditor(!showQueryEditor)} /> {showQueryEditor && ( )} {renderPlatformCompatibility()} {renderLiveQueryWarning()} {lastEditedQueryObserverCanRun && (
)} ); const renderForGlobalAdminOrAnyMaintainer = ( <>
{renderName()} {renderDescription()}
{isEditMode && renderAuthor()}
{renderPlatformCompatibility()} {isEditMode && ( <> setLastEditedQueryObserverCanRun(value) } wrapperClassName={`${baseClass}__query-observer-can-run-wrapper`} > Observers can run

Users with the Observer role will be able to run this query on hosts where they have access.

)} {renderLiveQueryWarning()}
{(hasSavePermissions || isAnyTeamMaintainerOrTeamAdmin) && ( <> {isEditMode && ( )}
{" "} <> You can only save
changes to a query if you
are the author.
)}
{isSaveModalOpen && ( )} ); if (isStoredQueryLoading) { return ; } if (isOnlyObserver || isGlobalObserver) { return renderRunForObserver; } return renderForGlobalAdminOrAnyMaintainer; }; export default QueryForm;