Spaces:
Build error
Build error
| import React from "react"; | |
| import { useSelector } from "react-redux"; | |
| import { IoAlertCircle } from "react-icons/io5"; | |
| import { useTranslation } from "react-i18next"; | |
| import { Editor, Monaco } from "@monaco-editor/react"; | |
| import { editor } from "monaco-editor"; | |
| import { Button, Select, SelectItem } from "@heroui/react"; | |
| import { useMutation } from "@tanstack/react-query"; | |
| import { RootState } from "#/store"; | |
| import { | |
| ActionSecurityRisk, | |
| SecurityAnalyzerLog, | |
| } from "#/state/security-analyzer-slice"; | |
| import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom"; | |
| import { I18nKey } from "#/i18n/declaration"; | |
| import toast from "#/utils/toast"; | |
| import InvariantLogoIcon from "./assets/logo"; | |
| import { getFormattedDateTime } from "#/utils/gget-formatted-datetime"; | |
| import { downloadJSON } from "#/utils/download-json"; | |
| import InvariantService from "#/api/invariant-service"; | |
| import { useGetPolicy } from "#/hooks/query/use-get-policy"; | |
| import { useGetRiskSeverity } from "#/hooks/query/use-get-risk-severity"; | |
| import { useGetTraces } from "#/hooks/query/use-get-traces"; | |
| type SectionType = "logs" | "policy" | "settings"; | |
| function SecurityInvariant() { | |
| const { t } = useTranslation(); | |
| const { logs } = useSelector((state: RootState) => state.securityAnalyzer); | |
| const [activeSection, setActiveSection] = React.useState("logs"); | |
| const [policy, setPolicy] = React.useState(""); | |
| const [selectedRisk, setSelectedRisk] = React.useState( | |
| ActionSecurityRisk.MEDIUM, | |
| ); | |
| const logsRef = React.useRef<HTMLDivElement>(null); | |
| useGetPolicy({ onSuccess: setPolicy }); | |
| useGetRiskSeverity({ | |
| onSuccess: (riskSeverity) => { | |
| setSelectedRisk( | |
| riskSeverity === 0 | |
| ? ActionSecurityRisk.LOW | |
| : riskSeverity || ActionSecurityRisk.MEDIUM, | |
| ); | |
| }, | |
| }); | |
| const { refetch: exportTraces } = useGetTraces({ | |
| onSuccess: (traces) => { | |
| toast.info(t(I18nKey.INVARIANT$TRACE_EXPORTED_MESSAGE)); | |
| const filename = `openhands-trace-${getFormattedDateTime()}.json`; | |
| downloadJSON(traces, filename); | |
| }, | |
| }); | |
| const { mutate: updatePolicy } = useMutation({ | |
| mutationFn: (variables: { policy: string }) => | |
| InvariantService.updatePolicy(variables.policy), | |
| onSuccess: () => { | |
| toast.info(t(I18nKey.INVARIANT$POLICY_UPDATED_MESSAGE)); | |
| }, | |
| }); | |
| const { mutate: updateRiskSeverity } = useMutation({ | |
| mutationFn: (variables: { riskSeverity: number }) => | |
| InvariantService.updateRiskSeverity(variables.riskSeverity), | |
| onSuccess: () => { | |
| toast.info(t(I18nKey.INVARIANT$SETTINGS_UPDATED_MESSAGE)); | |
| }, | |
| }); | |
| useScrollToBottom(logsRef); | |
| const getRiskColor = React.useCallback((risk: ActionSecurityRisk) => { | |
| switch (risk) { | |
| case ActionSecurityRisk.LOW: | |
| return "text-green-500"; | |
| case ActionSecurityRisk.MEDIUM: | |
| return "text-yellow-500"; | |
| case ActionSecurityRisk.HIGH: | |
| return "text-red-500"; | |
| case ActionSecurityRisk.UNKNOWN: | |
| default: | |
| return "text-gray-500"; | |
| } | |
| }, []); | |
| const getRiskText = React.useCallback( | |
| (risk: ActionSecurityRisk) => { | |
| switch (risk) { | |
| case ActionSecurityRisk.LOW: | |
| return t(I18nKey.SECURITY_ANALYZER$LOW_RISK); | |
| case ActionSecurityRisk.MEDIUM: | |
| return t(I18nKey.SECURITY_ANALYZER$MEDIUM_RISK); | |
| case ActionSecurityRisk.HIGH: | |
| return t(I18nKey.SECURITY_ANALYZER$HIGH_RISK); | |
| case ActionSecurityRisk.UNKNOWN: | |
| default: | |
| return t(I18nKey.SECURITY_ANALYZER$UNKNOWN_RISK); | |
| } | |
| }, | |
| [t], | |
| ); | |
| const handleEditorDidMount = React.useCallback( | |
| (_: editor.IStandaloneCodeEditor, monaco: Monaco): void => { | |
| monaco.editor.defineTheme("my-theme", { | |
| base: "vs-dark", | |
| inherit: true, | |
| rules: [], | |
| colors: { | |
| "editor.background": "#171717", | |
| }, | |
| }); | |
| monaco.editor.setTheme("my-theme"); | |
| }, | |
| [], | |
| ); | |
| const sections: Record<SectionType, React.ReactNode> = { | |
| logs: ( | |
| <> | |
| <div className="flex justify-between items-center border-b border-neutral-600 mb-4 p-4"> | |
| <h2 className="text-2xl">{t(I18nKey.INVARIANT$LOG_LABEL)}</h2> | |
| <Button onPress={() => exportTraces()} className="bg-tertiary"> | |
| {t(I18nKey.INVARIANT$EXPORT_TRACE_LABEL)} | |
| </Button> | |
| </div> | |
| <div | |
| className="flex-1 p-4 max-h-screen overflow-y-auto fast-smooth-scroll" | |
| ref={logsRef} | |
| > | |
| {logs.map((log: SecurityAnalyzerLog, index: number) => ( | |
| <div | |
| key={index} | |
| className={`mb-2 p-2 rounded-lg ${log.confirmed_changed && log.confirmation_state === "confirmed" ? "border-green-800" : "border-red-800"}`} | |
| style={{ | |
| backgroundColor: "rgba(128, 128, 128, 0.2)", | |
| borderWidth: log.confirmed_changed ? "2px" : "0", | |
| }} | |
| > | |
| <p className="text-sm relative break-words"> | |
| {log.content} | |
| {(log.confirmation_state === "awaiting_confirmation" || | |
| log.confirmed_changed) && ( | |
| <IoAlertCircle className="absolute top-0 right-0" /> | |
| )} | |
| </p> | |
| <p className={`text-xs ${getRiskColor(log.security_risk)}`}> | |
| {getRiskText(log.security_risk)} | |
| </p> | |
| </div> | |
| ))} | |
| </div> | |
| </> | |
| ), | |
| policy: ( | |
| <> | |
| <div className="flex justify-between items-center border-b border-neutral-600 mb-4 p-4"> | |
| <h2 className="text-2xl">{t(I18nKey.INVARIANT$POLICY_LABEL)}</h2> | |
| <Button | |
| className="bg-tertiary" | |
| onPress={() => updatePolicy({ policy })} | |
| > | |
| {t(I18nKey.INVARIANT$UPDATE_POLICY_LABEL)} | |
| </Button> | |
| </div> | |
| <div className="flex grow items-center justify-center"> | |
| <Editor | |
| path="policy.py" | |
| height="100%" | |
| onMount={handleEditorDidMount} | |
| value={policy} | |
| onChange={(value) => setPolicy(value || "")} | |
| /> | |
| </div> | |
| </> | |
| ), | |
| settings: ( | |
| <> | |
| <div className="flex justify-between items-center border-b border-neutral-600 mb-4 p-4"> | |
| <h2 className="text-2xl">{t(I18nKey.INVARIANT$SETTINGS_LABEL)}</h2> | |
| <Button | |
| className="bg-tertiary" | |
| onPress={() => updateRiskSeverity({ riskSeverity: selectedRisk })} | |
| > | |
| {t(I18nKey.INVARIANT$UPDATE_SETTINGS_LABEL)} | |
| </Button> | |
| </div> | |
| <div className="flex grow p-4"> | |
| <div className="flex flex-col w-full"> | |
| <p className="mb-2"> | |
| {t(I18nKey.INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL)} | |
| </p> | |
| <Select | |
| placeholder="Select risk severity" | |
| value={selectedRisk} | |
| onChange={(e) => | |
| setSelectedRisk(Number(e.target.value) as ActionSecurityRisk) | |
| } | |
| className={getRiskColor(selectedRisk)} | |
| selectedKeys={new Set([selectedRisk.toString()])} | |
| aria-label="Select risk severity" | |
| > | |
| <SelectItem | |
| key={ActionSecurityRisk.UNKNOWN} | |
| aria-label="Unknown Risk" | |
| className={getRiskColor(ActionSecurityRisk.UNKNOWN)} | |
| > | |
| {getRiskText(ActionSecurityRisk.UNKNOWN)} | |
| </SelectItem> | |
| <SelectItem | |
| key={ActionSecurityRisk.LOW} | |
| aria-label="Low Risk" | |
| className={getRiskColor(ActionSecurityRisk.LOW)} | |
| > | |
| {getRiskText(ActionSecurityRisk.LOW)} | |
| </SelectItem> | |
| <SelectItem | |
| key={ActionSecurityRisk.MEDIUM} | |
| aria-label="Medium Risk" | |
| className={getRiskColor(ActionSecurityRisk.MEDIUM)} | |
| > | |
| {getRiskText(ActionSecurityRisk.MEDIUM)} | |
| </SelectItem> | |
| <SelectItem | |
| key={ActionSecurityRisk.HIGH} | |
| aria-label="High Risk" | |
| className={getRiskColor(ActionSecurityRisk.HIGH)} | |
| > | |
| {getRiskText(ActionSecurityRisk.HIGH)} | |
| </SelectItem> | |
| <SelectItem | |
| key={ActionSecurityRisk.HIGH + 1} | |
| aria-label="Don't ask for confirmation" | |
| > | |
| {t(I18nKey.INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL)} | |
| </SelectItem> | |
| </Select> | |
| </div> | |
| </div> | |
| </> | |
| ), | |
| }; | |
| return ( | |
| <div className="flex flex-1 w-full h-full"> | |
| <div className="w-60 bg-base-secondary border-r border-r-neutral-600 p-4 flex-shrink-0"> | |
| <div className="text-center mb-2"> | |
| <InvariantLogoIcon className="mx-auto mb-1" /> | |
| <b>{t(I18nKey.INVARIANT$INVARIANT_ANALYZER_LABEL)}</b> | |
| </div> | |
| <p className="text-[0.6rem]"> | |
| {t(I18nKey.INVARIANT$INVARIANT_ANALYZER_MESSAGE)}{" "} | |
| <a | |
| className="underline" | |
| href="https://github.com/invariantlabs-ai/invariant" | |
| target="_blank" | |
| rel="noreferrer" | |
| > | |
| {t(I18nKey.INVARIANT$CLICK_TO_LEARN_MORE_LABEL)} | |
| </a> | |
| </p> | |
| <hr className="border-t border-neutral-600 my-2" /> | |
| <ul className="space-y-2"> | |
| <div | |
| className={`cursor-pointer p-2 rounded ${activeSection === "logs" && "bg-neutral-600"}`} | |
| onClick={() => setActiveSection("logs")} | |
| > | |
| {t(I18nKey.INVARIANT$LOG_LABEL)} | |
| </div> | |
| <div | |
| className={`cursor-pointer p-2 rounded ${activeSection === "policy" && "bg-neutral-600"}`} | |
| onClick={() => setActiveSection("policy")} | |
| > | |
| {t(I18nKey.INVARIANT$POLICY_LABEL)} | |
| </div> | |
| <div | |
| className={`cursor-pointer p-2 rounded ${activeSection === "settings" && "bg-neutral-600"}`} | |
| onClick={() => setActiveSection("settings")} | |
| > | |
| {t(I18nKey.INVARIANT$SETTINGS_LABEL)} | |
| </div> | |
| </ul> | |
| </div> | |
| <div className="flex flex-col min-h-0 w-full overflow-y-auto bg-base"> | |
| {sections[activeSection as SectionType]} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default SecurityInvariant; | |