Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LLM analyzers #508

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c067d0
Add initial version of LLM analyzer
rbs-jacob Oct 24, 2024
d9dcc13
Fix bugs when using in the UI
rbs-jacob Oct 24, 2024
e883681
Fix component configs default field=None bug
rbs-jacob Oct 24, 2024
24c1124
Update CHANGELOG
rbs-jacob Oct 24, 2024
9c34469
Merge branch 'bugfix/default-none-ui-components' into feature/llm-ana…
rbs-jacob Oct 24, 2024
57ef1ba
Merge branch 'master' into feature/llm-analyzer
rbs-jacob Oct 25, 2024
9642b36
Add first-pass AI analyzer UI
rbs-jacob Oct 29, 2024
9776803
Fix frontend folder copying
rbs-jacob Oct 29, 2024
87d9e7a
Add simple LLM test using Ollama
rbs-jacob Oct 30, 2024
3ae9c3c
Remove print statement
rbs-jacob Oct 30, 2024
fdb4789
Fix type errors
rbs-jacob Oct 30, 2024
cd629fd
Add LLM function analyzer
rbs-jacob Oct 30, 2024
486ee86
Improve function analyzer output quality
rbs-jacob Oct 31, 2024
6b3d750
Add whole-program LLM analyzer
rbs-jacob Oct 31, 2024
cc6ca8d
Add tests for LLM program analyzer
rbs-jacob Oct 31, 2024
d2ea6af
Merge branch 'master' into feature/llm-analyzer
rbs-jacob Oct 31, 2024
9ceb04e
Fix failing tests
rbs-jacob Nov 1, 2024
546eaf8
Update CHANGELOG
rbs-jacob Nov 1, 2024
025a694
Use the correct analyzer in the UI
rbs-jacob Nov 1, 2024
76672e4
Add explanatory docstring with install instructions
rbs-jacob Nov 4, 2024
3e1a1c1
Add LlmFunctionAnalyzer test case
rbs-jacob Nov 4, 2024
12cd0bb
Assert existence of attributes.description in tests
rbs-jacob Nov 4, 2024
f60e905
Show system prompt in textarea
rbs-jacob Nov 5, 2024
af754e1
Lint (oops)
rbs-jacob Nov 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/public/icons/ai.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 12 additions & 3 deletions frontend/src/resource/ResourceTreeToolbar.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script>
import AddTagView from "../views/AddTagView.svelte";
import AiView from "../views/AiView.svelte";
import CarveView from "../views/CarveView.svelte";
import CommentView from "../views/CommentView.svelte";
import ComponentsView from "../views/ComponentsView.svelte";
import ModifyView from "../views/ModifyView.svelte";
import RunScriptView from "../views/RunScriptView.svelte";
import ScriptView from "../views/ScriptView.svelte";
import SettingsView from "../views/SettingsView.svelte";
import SearchView from "../views/SearchView.svelte";
import AddTagView from "../views/AddTagView.svelte";
import RunScriptView from "../views/RunScriptView.svelte";
import SettingsView from "../views/SettingsView.svelte";
import Toolbar from "../utils/Toolbar.svelte";

import {
Expand Down Expand Up @@ -300,6 +301,14 @@
},
},

{
text: "Analyze with AI",
iconUrl: "/icons/ai.svg",
onclick: async (e) => {
modifierView = AiView;
},
},

{
text: "Show Script",
iconUrl: "/icons/document.svg",
Expand Down
211 changes: 211 additions & 0 deletions frontend/src/views/AiView.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<style>
.container {
min-height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: stretch;
align-content: center;
overflow: auto;
}

.inputs *:first-child {
margin-top: 0;
}

.output {
flex-grow: 1;
}

pre {
white-space: pre-wrap;
}

.actions {
margin-top: 2em;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: center;
align-content: center;
}

input {
background: inherit;
color: inherit;
border: none;
border-bottom: 1px solid white;
flex-grow: 1;
margin-left: 1ch;
}

input:focus,
textarea:focus {
outline: none;
box-shadow: inset 0 -1px 0 var(--main-fg-color);
}

textarea {
background: inherit;
color: inherit;
border: 1px solid white;
flex-grow: 1;
margin-left: 1ch;
resize: vertical;
font-size: inherit;
}

select {
margin-bottom: 1em;
background-color: inherit;
color: inherit;
border: 1px solid var(--main-fg-color);
border-radius: 0;
font-size: inherit;
font-family: var(--font);
box-shadow: none;
}

select:focus {
outline: none;
box-shadow: inset 0 -1px 0 var(--main-fg-color);
}

label {
margin-bottom: 1em;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: baseline;
align-content: center;
white-space: nowrap;
}
</style>

<script>
import Button from "../utils/Button.svelte";
import LoadingText from "../utils/LoadingText.svelte";

import { selectedResource } from "../stores.js";
import { onMount } from "svelte";

export let modifierView;
let apiUrl = JSON.parse(window.localStorage.getItem("aiApiUrl") ?? "null"),
model = JSON.parse(window.localStorage.getItem("aiApiModel") ?? "null"),
key = JSON.parse(window.localStorage.getItem("aiApiKey") ?? "null"),
prompt;
$: window.localStorage.setItem("aiApiUrl", JSON.stringify(apiUrl));
$: window.localStorage.setItem("aiApiModel", JSON.stringify(model));
$: window.localStorage.setItem("aiApiKey", JSON.stringify(key));

function getSystemPrompt(r) {
return r.get_config_for_component("LlmAnalyzer").then(({ fields }) => {
for (const field of fields) {
if (field.name == "system_prompt") {
prompt = field.default;
return field.default;
}
}
});
}
$: promptPromise = getSystemPrompt($selectedResource);

function getAnalyzer() {
if ($selectedResource.tags.includes("ofrak.core.program.Program")) {
return "LlmProgramAnalyzer";
}
if (
$selectedResource.tags.includes("ofrak.core.complex_block.ComplexBlock")
) {
return "LlmFunctionAnalyzer";
}
return "LlmAnalyzer";
}
$: analyzer = getAnalyzer();

let resultPromise = undefined;
function llmAnalyzer() {
resultPromise = fetch(
`${$selectedResource.uri}/run_component?component=${analyzer}`,
{
credentials: "omit",
body: JSON.stringify([
"ofrak.core.llm.LlmAnalyzerConfig",
{
api_url: apiUrl,
model: model,
api_key: key || undefined,
...(analyzer == "LlmAnalyzer" ? { system_prompt: prompt } : {}),
},
]),
method: "POST",
mode: "cors",
}
)
.then(async (r) => {
if (!r.ok) {
throw Error(JSON.stringify(await r.json(), undefined, 2));
}
return await r.json();
})
.then((r) => {
$selectedResource.attributes = $selectedResource.attributes;
return r;
})
.then(({ modified }) => {
const { attributes } = modified.find(
({ id }) => id == $selectedResource.resource_id
);
for (const [type, [_, { description }]] of attributes) {
if (type == "ofrak.core.llm.LlmAttributes") {
return description;
}
}
});
}
</script>

<div class="container">
{#await promptPromise}
<LoadingText />
{:then _}
<div class="inputs">
<select bind:value="{analyzer}">
<option value="LlmAnalyzer">LlmAnalyzer</option>
<option value="LlmFunctionAnalyzer">LlmFunctionAnalyzer</option>
<option value="LlmProgramAnalyzer">LlmProgramAnalyzer</option>
</select>
<label>
AI API URL
<input type="text" bind:value="{apiUrl}" />
</label>
<label>
AI Model
<input type="text" bind:value="{model}" />
</label>
<label>
AI API Key (Optional)
<input type="password" bind:value="{key}" />
</label>
{#if analyzer == "LlmAnalyzer"}
<label>
System Prompt (Optional)
<textarea value="{prompt}"></textarea>
</label>
{/if}
</div>
{:catch e}
<p>{e}</p>
{/await}
<pre class="output">{#await resultPromise}<LoadingText
/>{:then result}{#if result}{result}{/if}{:catch e}Try re-running the analyzer.
{e}{/await}</pre>
<div class="actions">
<Button on:click="{llmAnalyzer}">Analyze</Button>
<Button on:click="{() => (modifierView = undefined)}">Cancel</Button>
</div>
</div>
1 change: 1 addition & 0 deletions ofrak_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Add UEFI binary unpacker. ([#399](https://github.com/redballoonsecurity/ofrak/pull/399))
- Add recursive identify functionality in the GUI. ([#435](https://github.com/redballoonsecurity/ofrak/pull/435))
- Add generic DecompilationAnalysis classes. ([#453](https://github.com/redballoonsecurity/ofrak/pull/453))
- Add OFRAK AI analyzers that use LLMs to generate natural language representations of resources. ([#508](https://github.com/redballoonsecurity/ofrak/pull/508))

### Fixed
- Improved flushing of filesystem entries (including symbolic links and other types) to disk. ([#373](https://github.com/redballoonsecurity/ofrak/pull/373))
Expand Down
4 changes: 4 additions & 0 deletions ofrak_core/Dockerstub
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ RUN apt-get -y update && \
build-essential \
cmake \
cpio \
curl \
git \
genisoimage \
liblz4-dev \
Expand Down Expand Up @@ -75,3 +76,6 @@ RUN cd /tmp && \
make install && \
cd /tmp && \
rm -r UEFITool-A68

# Install Ollama
RUN curl -L "https://ollama.com/download/ollama-linux-""$TARGETARCH"".tgz" | tar -C /usr/ -xzv
Comment on lines +80 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want to add this to the Docker image?

This feels like an incomplete install step -- we're adding a dependency that requires the user to download/install more things to actually use it. The impact is a larger image size for all users, plus more work for users who actually want to use the LLM feature.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you propose doing it instead?

It would be ideal to have this whole thing in a separate package so that users could conditionally include the AI components. Then we could install Ollama and pull a model without worrying about unnecessarily inflating the core image. But one of the original constraints was that this should be in OFRAK core.

If we don't have Ollama installed, then we can't run the tests without having the tests themselves pull down and install the binary. As far as I know, in other OFRAK tests, there is no precedent for installing a dependency like this. It would be doable, but seems a little weird.

On the other hand, we could pull a model in the OFRAK core Dockerstub after installing Ollama, but this seems like a bad idea. We don't want to add another several-hundred-MB dependency to the OFRAK core images. We especially don't want to add models this large to the base image if the user is just going to use OpenAI anyway.

Installing Ollama, but not pulling a model, seemed like a reasonable tradeoff. If it seems wrong to you, I would appreciate guidance on a better way to do it.

1 change: 1 addition & 0 deletions ofrak_core/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ ofrak/gui/public:
npm install && \
npm run build && \
cd ../ofrak_core && \
rm -rf ofrak/gui/public ; \
rbs-jacob marked this conversation as resolved.
Show resolved Hide resolved
cp -r ../frontend/dist ofrak/gui/public ; \
fi
Loading
Loading