diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 4b958edeac86..09ef6a92abf0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -20,6 +20,9 @@ updates:
docusaurus:
patterns:
- "*docusaurus*"
+ eslint:
+ patterns:
+ - "*eslint*"
- package-ecosystem: "npm"
directory: "/docs"
@@ -30,3 +33,6 @@ updates:
docusaurus:
patterns:
- "*docusaurus*"
+ eslint:
+ patterns:
+ - "*eslint*"
diff --git a/config.template.toml b/config.template.toml
index 644e1d603472..0c5de48c8d63 100644
--- a/config.template.toml
+++ b/config.template.toml
@@ -182,6 +182,17 @@ llm_config = 'gpt3'
# Enable auto linting after editing
#enable_auto_lint = false
+#################################### Security ###################################
+# Configuration for security features
+##############################################################################
+[security]
+
+# Enable confirmation mode
+#confirmation_mode = true
+
+# The security analyzer to use
+#security_analyzer = ""
+
#################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation
# plugin for the available options
diff --git a/docs/package-lock.json b/docs/package-lock.json
index ec3fd838484d..48dc5039a350 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -17,7 +17,7 @@
"prism-react-renderer": "^2.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-icons": "^5.2.1",
+ "react-icons": "^5.3.0",
"react-use": "^17.5.1"
},
"devDependencies": {
@@ -13051,9 +13051,9 @@
}
},
"node_modules/react-icons": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
- "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
+ "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
"peerDependencies": {
"react": "*"
}
diff --git a/docs/package.json b/docs/package.json
index f5d4baa403e8..340423f649e1 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -24,7 +24,7 @@
"prism-react-renderer": "^2.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-icons": "^5.2.1",
+ "react-icons": "^5.3.0",
"react-use": "^17.5.1"
},
"devDependencies": {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 3e6ceb1ffa45..5ac85abda19c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -17,7 +17,7 @@
"@xterm/xterm": "^5.4.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
- "i18next": "^23.12.2",
+ "i18next": "^23.12.3",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"jose": "^5.6.3",
@@ -27,11 +27,11 @@
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.1",
- "react-icons": "^5.2.1",
+ "react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-syntax-highlighter": "^15.5.0",
- "tailwind-merge": "^2.5.1",
+ "tailwind-merge": "^2.5.2",
"vite": "^5.4.0",
"web-vitals": "^3.5.2"
},
@@ -60,7 +60,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.4",
"jsdom": "^24.1.1",
- "lint-staged": "^15.2.8",
+ "lint-staged": "^15.2.9",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.9",
@@ -8133,9 +8133,9 @@
}
},
"node_modules/i18next": {
- "version": "23.12.2",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz",
- "integrity": "sha512-XIeh5V+bi8SJSWGL3jqbTEBW5oD6rbP5L+E7dVQh1MNTxxYef0x15rhJVcRb7oiuq4jLtgy2SD8eFlf6P2cmqg==",
+ "version": "23.12.3",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.3.tgz",
+ "integrity": "sha512-DyigQmrR10V9U2N6pjhbfahW13GY7n8BQD9swN09JuRRropgsksWVi4vRLeex0Qf7zCPnBfIqQfhcBzdZBQBYw==",
"funding": [
{
"type": "individual",
@@ -9020,9 +9020,9 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/lint-staged": {
- "version": "15.2.8",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.8.tgz",
- "integrity": "sha512-PUWFf2zQzsd9EFU+kM1d7UP+AZDbKFKuj+9JNVTBkhUFhbg4MAt6WfyMMwBfM4lYqd4D2Jwac5iuTu9rVj4zCQ==",
+ "version": "15.2.9",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.9.tgz",
+ "integrity": "sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==",
"dev": true,
"dependencies": {
"chalk": "~5.3.0",
@@ -10921,9 +10921,9 @@
}
},
"node_modules/react-icons": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
- "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
+ "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
"peerDependencies": {
"react": "*"
}
@@ -12114,9 +12114,9 @@
}
},
"node_modules/tailwind-merge": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.1.tgz",
- "integrity": "sha512-1zKDdExKvNltulO+J0x/Rqv40xQn78FHsEQVn3rxt8e4HdebRIT6o6zGeLYlGuxd3Efue9Y69qsp8vKwEhuEeg==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
+ "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
diff --git a/frontend/package.json b/frontend/package.json
index 515d050a135d..f4be25e09fae 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,7 +16,7 @@
"@xterm/xterm": "^5.4.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
- "i18next": "^23.12.2",
+ "i18next": "^23.12.3",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"jose": "^5.6.3",
@@ -26,11 +26,11 @@
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.1",
- "react-icons": "^5.2.1",
+ "react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-syntax-highlighter": "^15.5.0",
- "tailwind-merge": "^2.5.1",
+ "tailwind-merge": "^2.5.2",
"vite": "^5.4.0",
"web-vitals": "^3.5.2"
},
@@ -83,7 +83,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.4",
"jsdom": "^24.1.1",
- "lint-staged": "^15.2.8",
+ "lint-staged": "^15.2.9",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.9",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 5cd81ecc815b..4d7f8816e16c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import { useDisclosure } from "@nextui-org/react";
import React, { useEffect } from "react";
import { Toaster } from "react-hot-toast";
+import { IoLockClosed } from "react-icons/io5";
import CogTooth from "#/assets/cog-tooth";
import ChatInterface from "#/components/chat/ChatInterface";
import Errors from "#/components/Errors";
@@ -15,13 +16,20 @@ import VolumeIcon from "./components/VolumeIcon";
import Terminal from "./components/terminal/Terminal";
import Session from "#/services/session";
import { getToken } from "#/services/auth";
-import { settingsAreUpToDate } from "#/services/settings";
+import { getSettings, settingsAreUpToDate } from "#/services/settings";
+import Security from "./components/modals/security/Security";
interface Props {
setSettingOpen: (isOpen: boolean) => void;
+ setSecurityOpen: (isOpen: boolean) => void;
+ showSecurityLock: boolean;
}
-function Controls({ setSettingOpen }: Props): JSX.Element {
+function Controls({
+ setSettingOpen,
+ setSecurityOpen,
+ showSecurityLock,
+}: Props): JSX.Element {
return (
@@ -33,6 +41,15 @@ function Controls({ setSettingOpen }: Props): JSX.Element {
+ {showSecurityLock && (
+
setSecurityOpen(true)}
+ >
+
+
+ )}
setSettingOpen(true)}
@@ -60,6 +77,14 @@ function App(): JSX.Element {
onOpenChange: onLoadPreviousSessionModalOpenChange,
} = useDisclosure();
+ const {
+ isOpen: securityModalIsOpen,
+ onOpen: onSecurityModalOpen,
+ onOpenChange: onSecurityModalOpenChange,
+ } = useDisclosure();
+
+ const { SECURITY_ANALYZER } = getSettings();
+
useEffect(() => {
if (initOnce) return;
initOnce = true;
@@ -98,11 +123,19 @@ function App(): JSX.Element {
secondClassName="flex flex-col overflow-hidden"
/>
-
+
+
{
diff --git a/frontend/src/components/modals/base-modal/BaseModal.tsx b/frontend/src/components/modals/base-modal/BaseModal.tsx
index 33015eca76f5..1d9934d231cb 100644
--- a/frontend/src/components/modals/base-modal/BaseModal.tsx
+++ b/frontend/src/components/modals/base-modal/BaseModal.tsx
@@ -13,6 +13,8 @@ interface BaseModalProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
title: string;
+ contentClassName?: string;
+ bodyClassName?: string;
isDismissable?: boolean;
subtitle?: string;
actions?: Action[];
@@ -24,6 +26,8 @@ function BaseModal({
isOpen,
onOpenChange,
title,
+ contentClassName = "max-w-[30rem] p-[40px]",
+ bodyClassName = "px-0 py-[20px]",
isDismissable = true,
subtitle = undefined,
actions = [],
@@ -42,14 +46,16 @@ function BaseModal({
size="sm"
className="bg-neutral-900 rounded-lg"
>
-
+
{(closeModal) => (
<>
-
-
-
+ {title && (
+
+
+
+ )}
- {children}
+ {children}
{actions && actions.length > 0 && (
diff --git a/frontend/src/components/modals/security/Security.tsx b/frontend/src/components/modals/security/Security.tsx
new file mode 100644
index 000000000000..1db90b68f4ed
--- /dev/null
+++ b/frontend/src/components/modals/security/Security.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import SecurityInvariant from "./invariant/Invariant";
+import BaseModal from "../base-modal/BaseModal";
+import { getSettings } from "#/services/settings";
+
+interface SecurityProps {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+}
+
+enum SecurityAnalyzerOption {
+ INVARIANT = "invariant",
+}
+
+const SecurityAnalyzers: Record = {
+ [SecurityAnalyzerOption.INVARIANT]: SecurityInvariant,
+};
+
+function Security({ isOpen, onOpenChange }: SecurityProps): JSX.Element {
+ const { SECURITY_ANALYZER } = getSettings();
+ const AnalyzerComponent =
+ SECURITY_ANALYZER &&
+ SecurityAnalyzers[SECURITY_ANALYZER as SecurityAnalyzerOption]
+ ? SecurityAnalyzers[SECURITY_ANALYZER as SecurityAnalyzerOption]
+ : () => Unknown security analyzer chosen
;
+
+ return (
+
+
+
+ );
+}
+
+export default Security;
diff --git a/frontend/src/components/modals/security/invariant/Invariant.tsx b/frontend/src/components/modals/security/invariant/Invariant.tsx
new file mode 100644
index 000000000000..fbd7340e8c82
--- /dev/null
+++ b/frontend/src/components/modals/security/invariant/Invariant.tsx
@@ -0,0 +1,324 @@
+import React, { useState, useRef, useCallback, useEffect } 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 "@nextui-org/react";
+import { RootState } from "#/store";
+import {
+ ActionSecurityRisk,
+ SecurityAnalyzerLog,
+} from "#/state/securityAnalyzerSlice";
+import { useScrollToBottom } from "#/hooks/useScrollToBottom";
+import { I18nKey } from "#/i18n/declaration";
+import { request } from "#/services/api";
+import toast from "#/utils/toast";
+import InvariantLogoIcon from "./assets/logo";
+
+type SectionType = "logs" | "policy" | "settings";
+
+function SecurityInvariant(): JSX.Element {
+ const { t } = useTranslation();
+ const { logs } = useSelector((state: RootState) => state.securityAnalyzer);
+ const [activeSection, setActiveSection] = useState("logs");
+
+ const logsRef = useRef(null);
+ const [policy, setPolicy] = useState("");
+ const [selectedRisk, setSelectedRisk] = useState(ActionSecurityRisk.MEDIUM);
+
+ useEffect(() => {
+ const fetchPolicy = async () => {
+ const data = await request(`/api/security/policy`);
+ setPolicy(data.policy);
+ };
+ const fetchRiskSeverity = async () => {
+ const data = await request(`/api/security/settings`);
+ setSelectedRisk(
+ data.RISK_SEVERITY === 0
+ ? ActionSecurityRisk.LOW
+ : data.RISK_SEVERITY || ActionSecurityRisk.MEDIUM,
+ );
+ };
+
+ fetchPolicy();
+ fetchRiskSeverity();
+ }, []);
+
+ useScrollToBottom(logsRef);
+
+ const getRiskColor = 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 = 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 = 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 getFormattedDateTime = () => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, "0");
+ const day = String(now.getDate()).padStart(2, "0");
+ const hour = String(now.getHours()).padStart(2, "0");
+ const minute = String(now.getMinutes()).padStart(2, "0");
+ const second = String(now.getSeconds()).padStart(2, "0");
+
+ return `${year}-${month}-${day}-${hour}-${minute}-${second}`;
+ };
+
+ // Function to download JSON data as a file
+ const downloadJSON = (data: object, filename: string) => {
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ async function exportTraces(): Promise {
+ const data = await request(`/api/security/export-trace`);
+ toast.info("Trace exported");
+
+ const filename = `opendevin-trace-${getFormattedDateTime()}.json`;
+ downloadJSON(data, filename);
+ }
+
+ async function updatePolicy(): Promise {
+ await request(`/api/security/policy`, {
+ method: "POST",
+ body: JSON.stringify({ policy }),
+ });
+ toast.info("Policy updated");
+ }
+
+ async function updateSettings(): Promise {
+ const payload = { RISK_SEVERITY: selectedRisk };
+ await request(`/api/security/settings`, {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+ toast.info("Settings updated");
+ }
+
+ const handleExportTraces = useCallback(() => {
+ exportTraces();
+ }, [exportTraces]);
+
+ const handleUpdatePolicy = useCallback(() => {
+ updatePolicy();
+ }, [updatePolicy]);
+
+ const handleUpdateSettings = useCallback(() => {
+ updateSettings();
+ }, [updateSettings]);
+
+ const sections: { [key in SectionType]: JSX.Element } = {
+ logs: (
+ <>
+
+
Logs
+
+
+
+ {logs.map((log: SecurityAnalyzerLog, index: number) => (
+
+
+ {log.content}
+ {(log.is_confirmed === "awaiting_confirmation" ||
+ log.confirmed_changed) && (
+
+ )}
+
+
+ {getRiskText(log.security_risk)}
+
+
+ ))}
+
+ >
+ ),
+ policy: (
+ <>
+
+
Policy
+
+
+
+ setPolicy(`${value}`)}
+ />
+
+ >
+ ),
+ settings: (
+ <>
+
+
Settings
+
+
+
+
+
Ask for user confirmation on risk severity:
+
+
+
+ >
+ ),
+ };
+
+ return (
+
+
+
+
+ Invariant Analyzer
+
+
+ Invariant Analyzer continuously monitors your OpenDevin agent for
+ security issues.{" "}
+
+ Click to learn more
+
+
+
+
+ setActiveSection("logs")}
+ >
+ Logs
+
+ setActiveSection("policy")}
+ >
+ Policy
+
+ setActiveSection("settings")}
+ >
+ Settings
+
+
+
+
+ {sections[activeSection as SectionType]}
+
+
+ );
+}
+
+export default SecurityInvariant;
diff --git a/frontend/src/components/modals/security/invariant/assets/logo.tsx b/frontend/src/components/modals/security/invariant/assets/logo.tsx
new file mode 100644
index 000000000000..2dc3192bda65
--- /dev/null
+++ b/frontend/src/components/modals/security/invariant/assets/logo.tsx
@@ -0,0 +1,80 @@
+import React from "react";
+
+interface InvariantLogoIconProps {
+ className?: string;
+}
+
+function InvariantLogoIcon({ className }: InvariantLogoIconProps): JSX.Element {
+ return (
+
+ );
+}
+
+export default InvariantLogoIcon;
diff --git a/frontend/src/components/modals/settings/AutocompleteCombobox.tsx b/frontend/src/components/modals/settings/AutocompleteCombobox.tsx
index 090b1a9055e5..4c309e999085 100644
--- a/frontend/src/components/modals/settings/AutocompleteCombobox.tsx
+++ b/frontend/src/components/modals/settings/AutocompleteCombobox.tsx
@@ -3,18 +3,20 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
-type Label = "model" | "agent" | "language";
+type Label = "model" | "agent" | "language" | "securityanalyzer";
const LABELS: Record