Merge pull request #5344 from ethereum/pastedCodeSafety

Pasted code safety
pull/5642/head
Aniket 3 weeks ago committed by GitHub
commit b1ef95f7be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/remix-ide/src/app/files/fileManager.ts
  2. 63
      apps/remix-ide/src/app/plugins/remixAIPlugin.tsx
  3. 1
      apps/remix-ide/src/app/tabs/locales/en/editor.json
  4. 184
      libs/remix-ai-core/src/agents/securityAgent.ts
  5. 3
      libs/remix-ai-core/src/helpers/streamHandler.ts
  6. 3
      libs/remix-ai-core/src/index.ts
  7. 2
      libs/remix-api/src/lib/plugins/remixAIDesktop-api.ts
  8. 9
      libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts
  9. 28
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx

@ -26,7 +26,7 @@ const profile = {
'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile',
'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath',
'saveCurrentFile', 'setBatchFiles', 'isGitRepo', 'isFile', 'isDirectory', 'hasGitSubmodule', 'copyFolderToJson', 'diff', 'saveCurrentFile', 'setBatchFiles', 'isGitRepo', 'isFile', 'isDirectory', 'hasGitSubmodule', 'copyFolderToJson', 'diff',
'hasGitSubmodules' 'hasGitSubmodules', 'getOpenedFiles'
], ],
kind: 'file-system' kind: 'file-system'
} }

@ -3,7 +3,7 @@ import { ViewPlugin } from '@remixproject/engine-web'
import { Plugin } from '@remixproject/engine'; import { Plugin } from '@remixproject/engine';
import { RemixAITab, ChatApi } from '@remix-ui/remix-ai' import { RemixAITab, ChatApi } from '@remix-ui/remix-ai'
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, CodeExplainAgent } from '@remix/remix-ai-core'; import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, CodeExplainAgent, SecurityAgent } from '@remix/remix-ai-core';
import { CustomRemixApi } from '@remix-api' import { CustomRemixApi } from '@remix-api'
import { PluginViewWrapper } from '@remix-ui/helper' import { PluginViewWrapper } from '@remix-ui/helper'
const _paq = (window._paq = window._paq || []) const _paq = (window._paq = window._paq || [])
@ -17,9 +17,8 @@ const profile = {
displayName: 'RemixAI', displayName: 'RemixAI',
methods: ['code_generation', 'code_completion', methods: ['code_generation', 'code_completion',
"solidity_answer", "code_explaining", "solidity_answer", "code_explaining",
"code_insertion", "error_explaining", "code_insertion", "error_explaining", "vulnerability_check",
"initialize", 'chatPipe', 'ProcessChatRequestBuffer', "initialize", 'chatPipe', 'ProcessChatRequestBuffer', 'isChatRequestPending'],
'isChatRequestPending'],
events: [], events: [],
icon: 'assets/img/remix-logo-blue.png', icon: 'assets/img/remix-logo-blue.png',
description: 'RemixAI provides AI services to Remix IDE.', description: 'RemixAI provides AI services to Remix IDE.',
@ -38,15 +37,16 @@ export class RemixAIPlugin extends ViewPlugin {
remoteInferencer:RemoteInferencer = null remoteInferencer:RemoteInferencer = null
isInferencing: boolean = false isInferencing: boolean = false
chatRequestBuffer: chatRequestBufferT<any> = null chatRequestBuffer: chatRequestBufferT<any> = null
agent: CodeExplainAgent codeExpAgent: CodeExplainAgent
securityAgent: SecurityAgent
useRemoteInferencer:boolean = false useRemoteInferencer:boolean = false
dispatch: any dispatch: any
constructor(inDesktop:boolean) { constructor(inDesktop:boolean) {
super(profile) super(profile)
this.isOnDesktop = inDesktop this.isOnDesktop = inDesktop
this.agent = new CodeExplainAgent(this) this.codeExpAgent = new CodeExplainAgent(this)
// user machine dont use resource for remote inferencing // user machine dont use ressource for remote inferencing
} }
onActivation(): void { onActivation(): void {
@ -62,6 +62,8 @@ export class RemixAIPlugin extends ViewPlugin {
this.useRemoteInferencer = true this.useRemoteInferencer = true
this.initialize() this.initialize()
} }
this.securityAgent = new SecurityAgent(this)
} }
async initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean){ async initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean){
@ -97,11 +99,6 @@ export class RemixAIPlugin extends ViewPlugin {
} }
async code_generation(prompt: string): Promise<any> { async code_generation(prompt: string): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
if (this.isOnDesktop && !this.useRemoteInferencer) { if (this.isOnDesktop && !this.useRemoteInferencer) {
return await this.call(this.remixDesktopPluginName, 'code_generation', prompt) return await this.call(this.remixDesktopPluginName, 'code_generation', prompt)
} else { } else {
@ -118,17 +115,8 @@ export class RemixAIPlugin extends ViewPlugin {
} }
async solidity_answer(prompt: string, params: IParams=GenerationParams): Promise<any> { async solidity_answer(prompt: string, params: IParams=GenerationParams): Promise<any> {
if (this.isInferencing) { const newPrompt = await this.codeExpAgent.chatCommand(prompt)
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
if (prompt.trimStart().startsWith('gpt') || prompt.trimStart().startsWith('sol-gpt')) {
params.terminal_output = true
params.stream_result = false
params.return_stream_response = false
}
const newPrompt = await this.agent.chatCommand(prompt)
let result let result
if (this.isOnDesktop && !this.useRemoteInferencer) { if (this.isOnDesktop && !this.useRemoteInferencer) {
result = await this.call(this.remixDesktopPluginName, 'solidity_answer', newPrompt) result = await this.call(this.remixDesktopPluginName, 'solidity_answer', newPrompt)
@ -142,11 +130,6 @@ export class RemixAIPlugin extends ViewPlugin {
} }
async code_explaining(prompt: string, context: string, params: IParams=GenerationParams): Promise<any> { async code_explaining(prompt: string, context: string, params: IParams=GenerationParams): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
let result let result
if (this.isOnDesktop && !this.useRemoteInferencer) { if (this.isOnDesktop && !this.useRemoteInferencer) {
result = await this.call(this.remixDesktopPluginName, 'code_explaining', prompt, context, params) result = await this.call(this.remixDesktopPluginName, 'code_explaining', prompt, context, params)
@ -159,11 +142,6 @@ export class RemixAIPlugin extends ViewPlugin {
} }
async error_explaining(prompt: string, context: string="", params: IParams=GenerationParams): Promise<any> { async error_explaining(prompt: string, context: string="", params: IParams=GenerationParams): Promise<any> {
if (this.isInferencing) {
this.call('terminal', 'log', { type: 'aitypewriterwarning', value: "RemixAI is already busy!" })
return
}
let result let result
if (this.isOnDesktop && !this.useRemoteInferencer) { if (this.isOnDesktop && !this.useRemoteInferencer) {
result = await this.call(this.remixDesktopPluginName, 'error_explaining', prompt) result = await this.call(this.remixDesktopPluginName, 'error_explaining', prompt)
@ -174,6 +152,22 @@ export class RemixAIPlugin extends ViewPlugin {
return result return result
} }
async vulnerability_check(prompt: string, params: IParams=GenerationParams): Promise<any> {
let result
if (this.isOnDesktop && !this.useRemoteInferencer) {
result = await this.call(this.remixDesktopPluginName, 'vulnerability_check', prompt)
} else {
result = await this.remoteInferencer.vulnerability_check(prompt, params)
}
if (result && params.terminal_output) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result })
return result
}
getVulnerabilityReport(file: string): any {
return this.securityAgent.getReport(file)
}
async code_insertion(msg_pfx: string, msg_sfx: string): Promise<any> { async code_insertion(msg_pfx: string, msg_sfx: string): Promise<any> {
if (this.isOnDesktop && !this.useRemoteInferencer) { if (this.isOnDesktop && !this.useRemoteInferencer) {
return await this.call(this.remixDesktopPluginName, 'code_insertion', msg_pfx, msg_sfx) return await this.call(this.remixDesktopPluginName, 'code_insertion', msg_pfx, msg_sfx)
@ -194,11 +188,12 @@ export class RemixAIPlugin extends ViewPlugin {
if (fn === "code_explaining") ChatApi.composer.send("Explain the current code") if (fn === "code_explaining") ChatApi.composer.send("Explain the current code")
else if (fn === "error_explaining") ChatApi.composer.send("Explain the error") else if (fn === "error_explaining") ChatApi.composer.send("Explain the error")
else if (fn === "solidity_answer") ChatApi.composer.send("Answer the following question") else if (fn === "solidity_answer") ChatApi.composer.send("Answer the following question")
else console.log("chatRequestBuffer is not empty. First process the last request.") else if (fn === "vulnerability_check") ChatApi.composer.send("Is there any vulnerability in the pasted code?")
else console.log("chatRequestBuffer function name not recognized.")
} }
} }
else { else {
console.log("chatRequestBuffer is not empty. First process the last request.") console.log("chatRequestBuffer is not empty. First process the last request.", this.chatRequestBuffer)
} }
_paq.push(['trackEvent', 'ai', 'remixAI_chat', 'askFromTerminal']) _paq.push(['trackEvent', 'ai', 'remixAI_chat', 'askFromTerminal'])
} }

@ -28,6 +28,7 @@
"editor.explainFunctionByAI": "```\n{content}\n```\nExplain the function {currentFunction}", "editor.explainFunctionByAI": "```\n{content}\n```\nExplain the function {currentFunction}",
"editor.explainFunctionByAISol": "```\n{content}\n```\nExplain the function {currentFunction}", "editor.explainFunctionByAISol": "```\n{content}\n```\nExplain the function {currentFunction}",
"editor.ExplainPipeMessage": "```\n {content}\n```\nExplain the snipped above", "editor.ExplainPipeMessage": "```\n {content}\n```\nExplain the snipped above",
"editor.PastedCodeSafety": "```\n {content}\n```\n\nReply in a short manner: Does this code contain major security vulnerabilities leading to a scam or loss of funds?",
"editor.executeFreeFunction": "Run a free function", "editor.executeFreeFunction": "Run a free function",
"editor.executeFreeFunction2": "Run the free function \"{name}\"", "editor.executeFreeFunction2": "Run the free function \"{name}\"",
"editor.toastText1": "This can only execute free function", "editor.toastText1": "This can only execute free function",

@ -1,28 +1,188 @@
// security checks // security checks
import * as fs from 'fs'; import * as fs from 'fs';
class SecurityAgent { interface SecurityReport {
private codebase: string[]; // list of codebase files compiled: boolean;
vulnerabilities: string[];
isNotSafe: string;
fileName: string;
reportTimestamp: string;
recommendations: string[];
fileModifiedSinceLastReport: boolean;
hasPastedCode: boolean;
}
class WorkspaceWatcher {
private intervalId: NodeJS.Timeout | null = null;
public interval: number;
private task: () => void;
constructor(task: () => void, interval: number) {
this.task = task;
this.interval = interval;
}
start(): void {
if (this.intervalId === null) {
this.intervalId = setInterval(() => {
this.task();
}, this.interval);
}
}
stop(): void {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
isRunning(): boolean {
return this.intervalId !== null;
}
}
export class SecurityAgent {
public currentFile: string; public currentFile: string;
public openedFiles: any;
private basePlugin: any;
private watcher: WorkspaceWatcher;
public reports: SecurityReport[] = [];
constructor(plugin) {
this.basePlugin = plugin;
this.basePlugin.on('fileManager', 'fileAdded', (path) => { });
this.basePlugin.on('fileManager', 'fileChanged', (path) => { //this.modifiedFile(path)
});
this.basePlugin.on('fileManager', 'fileRemoved', (path) => { this.removeFileFromReport(path) });
this.basePlugin.on('fileManager', 'fileRenamed', (oldName, newName) => {
this.removeFileFromReport(oldName);
this.addFileToReport(newName);
});
this.basePlugin.on('solidity', 'compilationFinished', async (fileName, source, languageVersion, data) => { this.onCompilationFinished(fileName) });
this.basePlugin.on('vyper', 'compilationFinished', async (fileName, source, languageVersion, data) => { this.onCompilationFinished(fileName) });
this.basePlugin.on('hardhat', 'compilationFinished', async (fileName, source, languageVersion, data) => { this.onCompilationFinished(fileName) });
this.basePlugin.on('foundry', 'compilationFinished', async (fileName, source, languageVersion, data) => { this.onCompilationFinished(fileName) });
this.watcher = new WorkspaceWatcher(async () => {
try {
this.currentFile = await this.basePlugin.call('fileManager', 'getCurrentFile');
this.openedFiles = await this.basePlugin.call('fileManager', 'getOpenedFiles');
Object.keys(this.openedFiles).forEach(key => {
this.addFileToReport(this.openedFiles[key]);
});
} catch (error) {
// no file selected or opened currently
}
}, 10000);
this.watcher.start();
}
addFileToReport(file: string): void {
const report = this.reports.find((r) => r.fileName === file);
if (report) {
// nothing to do
} else {
this.reports.push({
compiled: false,
isNotSafe: 'No',
vulnerabilities: [],
fileName: file,
reportTimestamp: null,
recommendations: [],
fileModifiedSinceLastReport: false,
hasPastedCode: false
});
}
constructor(codebasePath: string) {
// git or fs
this.codebase = this.loadCodebase(codebasePath);
} }
private loadCodebase(path: string): string[] { async onCompilationFinished(file: string) {
const files = fs.readdirSync(path); let report = this.reports.find((r) => r.fileName === file);
return files if (report) {
.filter(file => file.endsWith('.ts')) report.compiled = true;
.flatMap(file => fs.readFileSync(`${path}/${file}`, 'utf-8').split('\n')); report.fileModifiedSinceLastReport = false;
} else {
report = {
compiled: true,
isNotSafe: 'No',
vulnerabilities: [],
fileName: file,
reportTimestamp: null,
recommendations: [],
fileModifiedSinceLastReport: false,
hasPastedCode: false
}
this.reports.push(report);
}
try {
this.processFile(file);
console.log('Checking for vulnerabilities after compilation', this.reports);
} catch (error) {
console.error('Error checking for vulnerabilities after compilation: ', error);
}
// check for security vulnerabilities
} }
public update(currentFile, lineNumber){ removeFileFromReport(file: string): void {
const index = this.reports.findIndex((r) => r.fileName === file);
if (index !== -1) {
this.reports.splice(index, 1);
}
}
modifiedFile(file: string): void {
const report = this.reports.find((r) => r.fileName === file);
if (report) {
report.fileModifiedSinceLastReport = true;
}
}
async processFile(file: string) {
try {
const report = this.reports.find((r) => r.fileName === file);
if (report) { }
else {
this.reports.push({
compiled: false,
isNotSafe: 'No',
vulnerabilities: [],
fileName: file,
reportTimestamp: null,
recommendations: [],
fileModifiedSinceLastReport: false,
hasPastedCode: false
});
}
if (!report.reportTimestamp || report.fileModifiedSinceLastReport) {
const content = await this.basePlugin.call('fileManager', 'getFile', file);
const prompt = "```\n" + content + "\n```\n\nReply in a short manner: Does this code contain major security vulnerabilities leading to a scam or loss of funds?"
let result = await this.basePlugin.call('remixAI', 'vulnerability_check', prompt)
result = JSON.parse(result);
report.vulnerabilities = result.Reason;
report.recommendations = result.Suggestion;
report.isNotSafe = result.Answer;
report.reportTimestamp = new Date().toISOString();
}
} catch (error) {
console.error('Error processing file: ', error);
}
}
getReport(file: string): SecurityReport {
return this.reports.find((r) => r.fileName === file);
} }
public getRecommendations(currentLine: string, numSuggestions: number = 3): string[] { public getRecommendations(currentLine: string, numSuggestions: number = 3): string[] {
// process the codebase highlighting security vulnerabilities and deliver recommendations
const suggestions: string[] = []; const suggestions: string[] = [];
return suggestions; return suggestions;
} }

@ -46,6 +46,7 @@ export const HandleStreamResponse = async (streamResponse,
} }
catch (error) { catch (error) {
console.error('Error parsing JSON:', error); console.error('Error parsing JSON:', error);
return { 'generateText': 'Try again!', 'isGenerating': false }
} }
} }
if (done_cb) { if (done_cb) {
@ -54,7 +55,7 @@ export const HandleStreamResponse = async (streamResponse,
} }
catch (error) { catch (error) {
console.error('Error parsing JSON:', error); console.error('Error parsing JSON:', error);
return { 'generateText': '', 'isGenerating': false } return { 'generateText': 'Try again!', 'isGenerating': false }
} }
} }

@ -21,4 +21,5 @@ export {
export * from './types/types' export * from './types/types'
export * from './helpers/streamHandler' export * from './helpers/streamHandler'
export * from './agents/codeExplainAgent' export * from './agents/codeExplainAgent'
export * from './agents/securityAgent'

@ -10,7 +10,7 @@ export interface IRemixAID {
} & StatusEvents, } & StatusEvents,
methods: { methods: {
code_completion(context: string): Promise<string> code_completion(context: string): Promise<string>
code_insertion(msg_pfx: string, msg_sfx: string): Promise<string>, code_insertion(msg_pfx: string, msg_sfx: string): Promise<string>,
code_generation(prompt: string): Promise<string | null>, code_generation(prompt: string): Promise<string | null>,
code_explaining(code: string, context?: string): Promise<string | null>, code_explaining(code: string, context?: string): Promise<string | null>,

@ -9,7 +9,7 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
props: EditorUIProps props: EditorUIProps
monaco: any monaco: any
completionEnabled: boolean completionEnabled: boolean
task: string task: string = 'code_completion'
currentCompletion: any currentCompletion: any
private lastRequestTime: number = 0; private lastRequestTime: number = 0;
private readonly minRequestInterval: number = 200; private readonly minRequestInterval: number = 200;
@ -60,13 +60,6 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
endColumn: getTextAtLine(model.getLineCount()).length + 1, endColumn: getTextAtLine(model.getLineCount()).length + 1,
}); });
if (!word.endsWith(' ') &&
!word.endsWith('.') &&
!word.endsWith('"') &&
!word.endsWith('(')) {
return;
}
try { try {
const split = word.split('\n') const split = word.split('\n')
if (split.length < 2) return if (split.length < 2) return

@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import { isArray } from 'lodash' import { isArray } from 'lodash'
import Editor, { DiffEditor, loader, Monaco } from '@monaco-editor/react' import Editor, { DiffEditor, loader, Monaco } from '@monaco-editor/react'
import { AlertModal } from '@remix-ui/app' import { AppModal } from '@remix-ui/app'
import { ConsoleLogs, QueryParams } from '@remix-project/remix-lib' import { ConsoleLogs, QueryParams } from '@remix-project/remix-lib'
import { reducerActions, reducerListener, initialState } from './actions/editor' import { reducerActions, reducerListener, initialState } from './actions/editor'
import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solidity' import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solidity'
@ -664,11 +664,26 @@ export const EditorUI = (props: EditorUIProps) => {
} }
}) })
editor.onDidPaste((e) => { editor.onDidPaste(async (e) => {
if (!pasteCodeRef.current && e && e.range && e.range.startLineNumber >= 0 && e.range.endLineNumber >= 0 && e.range.endLineNumber - e.range.startLineNumber > 10) { if (!pasteCodeRef.current && e && e.range && e.range.startLineNumber >= 0 && e.range.endLineNumber >= 0 && e.range.endLineNumber - e.range.startLineNumber > 10) {
const modalContent: AlertModal = { // get the file name
const pastedCode = editor.getModel().getValueInRange(e.range)
const pastedCodePrompt = intl.formatMessage({ id: 'editor.PastedCodeSafety' }, { content:pastedCode })
const modalContent: AppModal = {
id: 'newCodePasted', id: 'newCodePasted',
title: intl.formatMessage({ id: 'editor.title1' }), title: "New code pasted",
okLabel: 'Ask RemixAI',
cancelLabel: 'Close',
cancelFn: () => {},
okFn: async () => {
await props.plugin.call('popupPanel', 'showPopupPanel', true)
setTimeout(async () => {
props.plugin.call('remixAI', 'chatPipe', 'vulnerability_check', pastedCodePrompt)
}, 500)
// add matamo event
_paq.push(['trackEvent', 'ai', 'remixAI', 'vulnerability_check_pasted_code'])
},
message: ( message: (
<div> <div>
{' '} {' '}
@ -699,10 +714,9 @@ export const EditorUI = (props: EditorUIProps) => {
</div> </div>
</div> </div>
</div> </div>
), )
} }
props.plugin.call('notification', 'alert', modalContent) props.plugin.call('notification', 'modal', modalContent)
pasteCodeRef.current = true
_paq.push(['trackEvent', 'editor', 'onDidPaste', 'more_than_10_lines']) _paq.push(['trackEvent', 'editor', 'onDidPaste', 'more_than_10_lines'])
} }
}) })

Loading…
Cancel
Save