Skip to content

Commit b0d8c1c

Browse files
committed
dotnet update
1 parent 3770f86 commit b0d8c1c

6 files changed

Lines changed: 2554 additions & 2119 deletions

File tree

dotnet_reversing/README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
## Description
44

5-
This agent is designed to perform reverse engineering and analysis of .NET binaries. It can decompile .NET assemblies and leverage a large language model (LLM) to analyze the source code based on a user-defined task, such as identifying security vulnerabilities. The agent can process binaries from a local file path or directly fetch them from the [NuGet package repository](https://www.nuget.org/packages). It operates asynchronously and can run multiple analysis instances in parallel.
5+
This agent is designed to perform reverse engineering and analysis of .NET binaries. It can decompile .NET assemblies and leverage an LLM to analyze the source code based on a user-defined task, such as identifying security vulnerabilities. The agent can process binaries from a local file path or directly fetch them from the [NuGet package repository](https://www.nuget.org/packages). It operates asynchronously and can run multiple analysis instances in parallel.
66

77
## Intended Use
88

9-
The primary purpose of this agent is to assist security researchers and developers in automating the process of scanning .NET applications for potential security flaws. A user can provide a high-level task, like "Find only critical vulnerabilities," and the agent will use its tools to decompile the code and use an LLM to analyze it, reporting any findings. It can also be used as a simple utility to decompile and view the source code of .NET assemblies.
9+
The primary purpose of this agent is to assist security researchers and developers in automating the process of scanning .NET applications for potential security flaws.
1010

1111
## Environment
1212

13-
The agent is a command-line application built with Python. It requires a Python environment with the necessary libraries installed, as specified in the script. It interacts with the public [NuGet API](https://learn.microsoft.com/en-us/nuget/api/overview) (api.nuget.org) to fetch packages. For its analysis capabilities, it relies on a configured language model, which can be a remote API (like GPT-4o-mini) or a locally hosted model (e.g., via Ollama). For observability and task tracking, it can be optionally [connected to a Dreadnode server](https://docs.dreadnode.io/strikes/usage/config).
13+
It interacts with the public [NuGet API](https://learn.microsoft.com/en-us/nuget/api/overview) (api.nuget.org) to fetch packages, or with local dotnet assemblies.
1414

1515
## Tools
1616

@@ -25,16 +25,16 @@ The agent is a command-line application built with Python. It requires a Python
2525
- `search_for_references`
2626
- `get_call_flows_to_method`
2727

28-
## Features
29-
30-
- **Multi-Source Analysis**: Capable of analyzing .NET binaries from local paths, directories, or directly from NuGet packages.
31-
- **LLM-Powered Analysis**: Utilizes a configurable language model to intelligently analyze decompiled source code based on a custom task.
32-
- **Vulnerability Reporting**: Can identify and report findings, classifying them by criticality (critical, high, medium, low, info).
33-
- **Concurrent Execution**: Supports running multiple agent instances in parallel to speed up the analysis of many binaries.
34-
- **Source Code Dumping**: Includes a utility to decompile and save the source code of specified binaries to a text file.
35-
- **Iterative Analysis**: Performs analysis in an iterative loop, with a configurable maximum number of steps to prevent infinite runs.
36-
- **Task Completion Summary**: Provides a final summary upon task completion, indicating success or failure and a brief markdown report.
37-
3828
## References
3929

4030
- [ILSpy](https://github.com/icsharpcode/ILSpy)
31+
32+
## Examples
33+
34+
```bash
35+
uv run dotnet_reversing/main.py --model "anthropic/claude-haiku-4-5-20251001" --path ./dotnet_reversing/example_binaries/
36+
```
37+
38+
## Notes
39+
40+
It requires access to dotnet, and for dotnet to be in your path, `export DOTNET_ROOT=~/.dotnet`
Binary file not shown.

dotnet_reversing/main.py

Lines changed: 57 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import io
2-
import sys
32
import typing as t
43
import zipfile
54
from dataclasses import dataclass
@@ -9,11 +8,15 @@
98
import aiohttp
109
import cyclopts
1110
import dreadnode as dn
12-
import litellm
13-
import rigging as rg
14-
from loguru import logger
11+
from dreadnode.agent import Agent
12+
from dreadnode.agent.events import AgentStart
13+
from dreadnode.agent.hooks import Hook
14+
from dreadnode.agent.tools import tool
15+
from dreadnode.data_types import Markdown
16+
from reversing import DotnetReversingTool
17+
from rich.console import Console
1518

16-
from dotnet_reversing.reversing import DotnetReversing
19+
console = Console()
1720

1821
if t.TYPE_CHECKING:
1922
from loguru import Record as LogRecord
@@ -32,7 +35,7 @@ class Args:
3235
"""Binary or directory of binaries to analyze or other supported identifier"""
3336
nuget: bool = False
3437
"""Treat the path as a NuGet package id or path to a list of packages"""
35-
task: str = "Find only critical vulnerabilities"
38+
task: str = "Find all useful vulnerabilities"
3639
"""Task presented to the agent"""
3740
max_steps: int = 25
3841
"""Maximum number of iterations per agent"""
@@ -49,7 +52,7 @@ class DreadnodeArgs:
4952
"""Dreadnode server URL"""
5053
token: str | None = None
5154
"""Dreadnode API token"""
52-
project: str = "dotnet-reversing"
55+
project: str = "dotnet-reversing-example"
5356
"""Project name"""
5457
console: t.Annotated[bool, cyclopts.Parameter(negative=False)] = False
5558
"""Show span information in the console"""
@@ -65,7 +68,7 @@ def log_formatter(record: "LogRecord") -> str:
6568
)
6669

6770

68-
@dn.task(name="Report finding", log_inputs=False, log_output=False)
71+
@tool()
6972
async def report_finding(file: str, method: str, criticality: str, content: str) -> str:
7073
"""
7174
Report a finding regarding areas or interest or vulnerabilities.
@@ -77,9 +80,7 @@ async def report_finding(file: str, method: str, criticality: str, content: str)
7780
- "low"
7881
- "info"
7982
"""
80-
logger.success(f"Reporting finding for {file} ({method}) [{criticality}]:")
81-
logger.info(content)
82-
logger.info("---")
83+
8384
dn.log_output(
8485
"finding",
8586
{
@@ -95,30 +96,27 @@ async def report_finding(file: str, method: str, criticality: str, content: str)
9596
return "Reported"
9697

9798

98-
@dn.task(name="Finish task", log_output=False)
99-
async def finish_task(success: bool, markdown_summary: str) -> None:
99+
@tool()
100+
async def finish_task(success: bool, markdown_summary: str) -> str:
100101
"""
101102
Mark your task as complete with a success/failure status and markdown summary.
102103
"""
103104
dn.log_metric("task_success", success)
104105
if success:
105106
dn.tag("success", to="run")
106107

107-
log_func = logger.success if success else logger.warning
108-
log_func(f"Agent finished the task (success={success}): {markdown_summary}")
109-
110108
dn.log_metric("task_success", success, to="run")
111-
dn.log_output("task_summary", markdown_summary, to="run")
109+
dn.log_output("task_summary", Markdown(markdown_summary), to="run")
110+
111+
return "Task Finished"
112112

113113

114-
@dn.task(name="Download NuGet package")
115114
async def download_nuget_package(package: str) -> Path:
116115
"""
117116
Download a NuGet package and return the path to the package.
118117
"""
119118

120119
package = package.lower()
121-
logger.info(f"Downloading NuGet package {package}...")
122120

123121
async with aiohttp.ClientSession() as client:
124122
# Get the versions
@@ -131,7 +129,6 @@ async def download_nuget_package(package: str) -> Path:
131129
data = await response.json()
132130
versions = data["versions"]
133131
latest_version = versions[-1]
134-
logger.info(f" |- Latest version is {latest_version}")
135132

136133
# Download the nupkg and extract it
137134
async with client.get(
@@ -147,84 +144,16 @@ async def download_nuget_package(package: str) -> Path:
147144
with io.BytesIO(data) as buffer, zipfile.ZipFile(buffer) as zip_file:
148145
zip_file.extractall(output_dir)
149146

150-
logger.info(f" |- Extracted to {output_dir}")
151-
152147
return output_dir
153148

154149

155-
async def agent(args: Args) -> None:
156-
with (
157-
dn.run(),
158-
dn.task_span("Agent"),
159-
logger.contextualize(prefix=str(args.path)),
160-
):
161-
dn.log_params(
162-
model=args.model,
163-
path=str(args.path),
164-
nuget=args.nuget,
165-
task=args.task,
166-
max_steps=args.max_steps,
167-
)
168-
169-
path = await download_nuget_package(args.path) if args.nuget else Path(args.path)
170-
reversing = DotnetReversing.from_path(path)
171-
172-
logger.info(f"Analyzing the following binaries with the goal: '{args.task}':")
173-
for binary in reversing.binaries:
174-
logger.info(f" |- {binary}")
150+
def upload_package_hook(
151+
package_path: str,
152+
) -> Hook:
153+
async def upload_package(event: AgentStart) -> None:
154+
dn.log_artifact(package_path)
175155

176-
dn.log_inputs(
177-
binaries=[str(b) for b in reversing.binaries],
178-
)
179-
180-
binary_list = "\n".join(reversing.binaries)
181-
182-
prompt = dedent(f"""\
183-
Analyze the following binaries and resolve the task below using all the tools available to you.
184-
Provide a report for all interesting findings you discover while performing the task.
185-
186-
<task>
187-
{args.task}
188-
</task>
189-
190-
<files>
191-
{binary_list}
192-
</files>
193-
""")
194-
195-
dn.log_input("task", args.task, to="run")
196-
dn.log_input("binaries", binary_list, to="run")
197-
198-
generator = rg.get_generator(args.model)
199-
chat = (
200-
await generator.chat(prompt)
201-
.catch(
202-
*litellm.exceptions.LITELLM_EXCEPTION_TYPES,
203-
on_failed="include",
204-
)
205-
.using(
206-
reversing.tools,
207-
report_finding,
208-
finish_task,
209-
max_depth=args.max_steps,
210-
)
211-
.cache("latest")
212-
.run()
213-
)
214-
215-
if chat.failed and chat.error:
216-
if isinstance(chat.error, rg.error.MaxDepthError):
217-
logger.warning(f"Max steps reached ({args.max_steps})")
218-
dn.log_metric("max_steps_reached", 1)
219-
dn.log_output("task_summary", f"Max steps ({args.max_steps}) reached", to="run")
220-
else:
221-
logger.warning(f"Failed with {chat.error}")
222-
dn.log_metric("inference_failed", 1)
223-
dn.log_output("task_summary", f"Inference failed with {chat.error}", to="run")
224-
225-
elif chat.last.role == "assistant":
226-
dn.log_output("last_message", chat.last.content)
227-
logger.info(str(chat.last))
156+
return upload_package
228157

229158

230159
@app.default
@@ -233,10 +162,6 @@ async def main(*, args: Args, dn_args: DreadnodeArgs | None = None) -> None:
233162
Agent to analyze .NET binaries and report findings.
234163
"""
235164

236-
logger.remove()
237-
logger.add(sys.stderr, format=log_formatter, level=args.log_level)
238-
logger.enable("rigging")
239-
240165
dn_args = dn_args or DreadnodeArgs()
241166
dn.configure(
242167
server=dn_args.server,
@@ -245,37 +170,46 @@ async def main(*, args: Args, dn_args: DreadnodeArgs | None = None) -> None:
245170
console=dn_args.console,
246171
)
247172

248-
await agent(args)
173+
path = await download_nuget_package(args.path) if args.nuget else Path(args.path)
174+
reversing = DotnetReversingTool(variant="all", base_path=path)
249175

250-
logger.info("Done.")
176+
binary_list = "\n".join(reversing.binaries)
251177

178+
instructions = dedent("""\
179+
You are a .NET reversing expert.
252180
253-
@app.command
254-
async def dump(*, args: Args, dn_args: DreadnodeArgs | None = None) -> None:
255-
"""
256-
Dump the source code of the binaries in the specified path.
257-
"""
258-
logger.remove()
259-
logger.add(sys.stderr, format=log_formatter, level=args.log_level)
260-
logger.enable("rigging")
181+
Use the Dotnet Reversing tool to decompile and analyze the binaries.
182+
For each finding, use the Report Finding tool to log your discoveries.
261183
262-
dn_args = dn_args or DreadnodeArgs()
263-
dn.configure(
264-
server=dn_args.server,
265-
token=dn_args.token,
266-
project=dn_args.project,
267-
console=dn_args.console,
184+
Once you have completed your analysis, use the Finish Task tool to summarize your findings
185+
and indicate whether you successfully completed the task.
186+
""")
187+
188+
user_input = dedent(f"""\
189+
Here is your task:
190+
191+
<task>
192+
{args.task}
193+
</task>
194+
195+
<files>
196+
{binary_list}
197+
</files>
198+
""")
199+
200+
agent = Agent(
201+
name="Dotnet Reversing Agent",
202+
description="Agent to analyze .NET binaries and report findings.",
203+
model=args.model,
204+
instructions=instructions,
205+
tools=[reversing, report_finding, finish_task],
206+
max_steps=args.max_steps,
207+
hooks=[upload_package_hook(str(path))],
268208
)
269209

270-
path = await download_nuget_package(args.path) if args.nuget else Path(args.path)
271-
reversing = DotnetReversing.from_path(path)
272-
273-
for binary in reversing.binaries:
274-
logger.info(f"Dumping source code for {binary}...")
275-
source_code = reversing.decompile_module(binary)
276-
output_file = Path(f"{binary}_source.txt")
277-
output_file.write_text(source_code)
278-
logger.success(f"Source code dumped to {output_file}")
210+
async with agent.stream(user_input) as events:
211+
async for event in events:
212+
console.print(event)
279213

280214

281215
if __name__ == "__main__":

0 commit comments

Comments
 (0)