dreadnode 1.0.0rc0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.3
2
+ Name: dreadnode
3
+ Version: 1.0.0rc0
4
+ Summary: Dreadnode SDK
5
+ Author: Nick Landers
6
+ Author-email: monoxgas@gmail.com
7
+ Requires-Python: >=3.10,<3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Provides-Extra: training
13
+ Requires-Dist: coolname (>=2.2.0,<3.0.0)
14
+ Requires-Dist: fast-depends (>=2.4.12,<3.0.0)
15
+ Requires-Dist: fsspec[s3] (>=2023.1.0,<=2025.3.0)
16
+ Requires-Dist: httpx (>=0.28.0,<0.29.0)
17
+ Requires-Dist: logfire (>=3.5.3,<4.0.0)
18
+ Requires-Dist: pydantic (>=2.9.2,<3.0.0)
19
+ Requires-Dist: python-ulid (>=3.0.0,<4.0.0)
20
+ Requires-Dist: transformers (>=4.41.0,<5.0.0) ; extra == "training"
21
+ Project-URL: Repository, https://github.com/dreadnode/sdk
22
+ Description-Content-Type: text/markdown
23
+
24
+ <p align="center">
25
+ <img
26
+ src="https://d1lppblt9t2x15.cloudfront.net/logos/5714928f3cdc09503751580cffbe8d02.png"
27
+ alt="Logo"
28
+ align="center"
29
+ width="144px"
30
+ height="144px"
31
+ />
32
+ </p>
33
+
34
+ <h3 align="center">
35
+ Dreadnode Strikes SDK
36
+ </h3>
37
+
38
+ <h4 align="center">
39
+ <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/dreadnode">
40
+ <img alt="PyPI - Version" src="https://img.shields.io/pypi/v/dreadnode">
41
+ <img alt="GitHub License" src="https://img.shields.io/github/license/dreadnode/sdk">
42
+ <img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/dreadnode/sdk/tests.yaml">
43
+ <img alt="Pre-Commit" src="https://img.shields.io/github/actions/workflow/status/dreadnode/sdk/pre-commit.yaml">
44
+ <img alt="Renovate" src="https://img.shields.io/github/actions/workflow/status/dreadnode/sdk/renovate.yaml">
45
+ </h4>
46
+
47
+ </br>
48
+
49
+ Strikes is an platform for building, experimenting with, and evaluating AI security agent code.
50
+
51
+ - **Experiment + Tasking + Observability** in a single place that's lightweight and scales.
52
+ - **Track your data** with parameters, inputs, and outputs all connected to your tasks.
53
+ - **Measure everything** with metrics throughout your code and anywhere you need them.
54
+ - **Scale your code** from a single run to thousands.
55
+
56
+ ```python
57
+ import dreadnode as dn
58
+ import rigging as rg
59
+
60
+ from .tools import reversing_tools
61
+
62
+ dn.configure()
63
+
64
+ @dataclass
65
+ class Finding:
66
+ name: str
67
+ severity: str
68
+ description: str
69
+ exploit_code: str
70
+
71
+ @dn.scorer(name="Score Finding")
72
+ async def score_finding(finding: Finding) -> float:
73
+ if finding.severity == "critical":
74
+ return 1.0
75
+ elif finding.severity == "high":
76
+ return 0.8
77
+ else:
78
+ return 0.2
79
+
80
+ @dn.task(scorers=[score_finding])
81
+ @rg.prompt(tools=[reversing_tools])
82
+ async def analyze_binary(binary: str) -> list[Finding]:
83
+ """
84
+ Analyze the binary for vulnerabilities.
85
+ """
86
+ ...
87
+
88
+ with dn.run(tags=["reverse-engineering"]):
89
+ binary = "c2/downloads/service.exe"
90
+
91
+ dn.log_params(
92
+ model="gpt-4",
93
+ temperature=0.5,
94
+ binary=binary
95
+ )
96
+
97
+ findings = await analyze_binary(binary)
98
+
99
+ dn.log_metric("findings", len(findings))
100
+ ```
101
+
102
+ ## Installation
103
+
104
+ We publish every version to PyPi:
105
+ ```bash
106
+ pip install -U dreadnode
107
+ ```
108
+
109
+ If you want to build from source:
110
+ ```bash
111
+ poetry install
112
+ ```
113
+
114
+ See our **[installation guide](https://docs.dreadnode.io/strikes/install)** for more options.
115
+
116
+ ## Getting Started
117
+
118
+ Read through our **[introduction guide](https://docs.dreadnode.io/strikes/intro)** in the docs.
119
+
120
+ ## Examples
121
+
122
+ Check out **[dreadnode/example-agents](https://github.com/dreadnode/example-agents)** to find your favorite use case.
@@ -0,0 +1,99 @@
1
+ <p align="center">
2
+ <img
3
+ src="https://d1lppblt9t2x15.cloudfront.net/logos/5714928f3cdc09503751580cffbe8d02.png"
4
+ alt="Logo"
5
+ align="center"
6
+ width="144px"
7
+ height="144px"
8
+ />
9
+ </p>
10
+
11
+ <h3 align="center">
12
+ Dreadnode Strikes SDK
13
+ </h3>
14
+
15
+ <h4 align="center">
16
+ <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/dreadnode">
17
+ <img alt="PyPI - Version" src="https://img.shields.io/pypi/v/dreadnode">
18
+ <img alt="GitHub License" src="https://img.shields.io/github/license/dreadnode/sdk">
19
+ <img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/dreadnode/sdk/tests.yaml">
20
+ <img alt="Pre-Commit" src="https://img.shields.io/github/actions/workflow/status/dreadnode/sdk/pre-commit.yaml">
21
+ <img alt="Renovate" src="https://img.shields.io/github/actions/workflow/status/dreadnode/sdk/renovate.yaml">
22
+ </h4>
23
+
24
+ </br>
25
+
26
+ Strikes is an platform for building, experimenting with, and evaluating AI security agent code.
27
+
28
+ - **Experiment + Tasking + Observability** in a single place that's lightweight and scales.
29
+ - **Track your data** with parameters, inputs, and outputs all connected to your tasks.
30
+ - **Measure everything** with metrics throughout your code and anywhere you need them.
31
+ - **Scale your code** from a single run to thousands.
32
+
33
+ ```python
34
+ import dreadnode as dn
35
+ import rigging as rg
36
+
37
+ from .tools import reversing_tools
38
+
39
+ dn.configure()
40
+
41
+ @dataclass
42
+ class Finding:
43
+ name: str
44
+ severity: str
45
+ description: str
46
+ exploit_code: str
47
+
48
+ @dn.scorer(name="Score Finding")
49
+ async def score_finding(finding: Finding) -> float:
50
+ if finding.severity == "critical":
51
+ return 1.0
52
+ elif finding.severity == "high":
53
+ return 0.8
54
+ else:
55
+ return 0.2
56
+
57
+ @dn.task(scorers=[score_finding])
58
+ @rg.prompt(tools=[reversing_tools])
59
+ async def analyze_binary(binary: str) -> list[Finding]:
60
+ """
61
+ Analyze the binary for vulnerabilities.
62
+ """
63
+ ...
64
+
65
+ with dn.run(tags=["reverse-engineering"]):
66
+ binary = "c2/downloads/service.exe"
67
+
68
+ dn.log_params(
69
+ model="gpt-4",
70
+ temperature=0.5,
71
+ binary=binary
72
+ )
73
+
74
+ findings = await analyze_binary(binary)
75
+
76
+ dn.log_metric("findings", len(findings))
77
+ ```
78
+
79
+ ## Installation
80
+
81
+ We publish every version to PyPi:
82
+ ```bash
83
+ pip install -U dreadnode
84
+ ```
85
+
86
+ If you want to build from source:
87
+ ```bash
88
+ poetry install
89
+ ```
90
+
91
+ See our **[installation guide](https://docs.dreadnode.io/strikes/install)** for more options.
92
+
93
+ ## Getting Started
94
+
95
+ Read through our **[introduction guide](https://docs.dreadnode.io/strikes/intro)** in the docs.
96
+
97
+ ## Examples
98
+
99
+ Check out **[dreadnode/example-agents](https://github.com/dreadnode/example-agents)** to find your favorite use case.
@@ -0,0 +1,51 @@
1
+ from dreadnode.main import DEFAULT_INSTANCE, Dreadnode
2
+ from dreadnode.metric import Metric, MetricDict, Scorer
3
+ from dreadnode.object import Object
4
+ from dreadnode.task import Task
5
+ from dreadnode.tracing.span import RunSpan, Span, TaskSpan
6
+ from dreadnode.version import VERSION
7
+
8
+ configure = DEFAULT_INSTANCE.configure
9
+ shutdown = DEFAULT_INSTANCE.shutdown
10
+
11
+ api = DEFAULT_INSTANCE.api
12
+ span = DEFAULT_INSTANCE.span
13
+ task = DEFAULT_INSTANCE.task
14
+ task_span = DEFAULT_INSTANCE.task_span
15
+ run = DEFAULT_INSTANCE.run
16
+ scorer = DEFAULT_INSTANCE.scorer
17
+ task_span = DEFAULT_INSTANCE.task_span
18
+ push_update = DEFAULT_INSTANCE.push_update
19
+
20
+ log_metric = DEFAULT_INSTANCE.log_metric
21
+ log_param = DEFAULT_INSTANCE.log_param
22
+ log_params = DEFAULT_INSTANCE.log_params
23
+ log_input = DEFAULT_INSTANCE.log_input
24
+ log_inputs = DEFAULT_INSTANCE.log_inputs
25
+ log_output = DEFAULT_INSTANCE.log_output
26
+ link_objects = DEFAULT_INSTANCE.link_objects
27
+ log_artifact = DEFAULT_INSTANCE.log_artifact
28
+
29
+ __version__ = VERSION
30
+
31
+ __all__ = [
32
+ "Dreadnode",
33
+ "Metric",
34
+ "MetricDict",
35
+ "Object",
36
+ "Run",
37
+ "RunSpan",
38
+ "Score",
39
+ "Scorer",
40
+ "Span",
41
+ "Task",
42
+ "TaskSpan",
43
+ "__version__",
44
+ "configure",
45
+ "log_metric",
46
+ "log_param",
47
+ "run",
48
+ "shutdown",
49
+ "span",
50
+ "task",
51
+ ]
File without changes
@@ -0,0 +1,249 @@
1
+ import io
2
+ import json
3
+ import typing as t
4
+
5
+ import httpx
6
+ import pandas as pd
7
+ from pydantic import BaseModel
8
+ from ulid import ULID
9
+
10
+ from dreadnode.util import logger
11
+ from dreadnode.version import VERSION
12
+
13
+ from .models import (
14
+ MetricAggregationType,
15
+ Project,
16
+ Run,
17
+ StatusFilter,
18
+ Task,
19
+ TimeAggregationType,
20
+ TimeAxisType,
21
+ TraceSpan,
22
+ UserDataCredentials,
23
+ )
24
+
25
+ ModelT = t.TypeVar("ModelT", bound=BaseModel)
26
+
27
+
28
+ class ApiClient:
29
+ """Client for the Dreadnode API."""
30
+
31
+ def __init__(
32
+ self,
33
+ base_url: str,
34
+ api_key: str,
35
+ *,
36
+ debug: bool = False,
37
+ ):
38
+ self._base_url = base_url.rstrip("/")
39
+ if not self._base_url.endswith("/api"):
40
+ self._base_url += "/api"
41
+
42
+ self._client = httpx.Client(
43
+ headers={
44
+ "User-Agent": f"dreadnode-sdk/{VERSION}",
45
+ "Accept": "application/json",
46
+ "X-API-Key": api_key,
47
+ },
48
+ base_url=self._base_url,
49
+ timeout=30,
50
+ )
51
+
52
+ if debug:
53
+ self._client.event_hooks["request"].append(self._log_request)
54
+ self._client.event_hooks["response"].append(self._log_response)
55
+
56
+ def _log_request(self, request: httpx.Request) -> None:
57
+ """Log every request to the console if debug is enabled."""
58
+
59
+ logger.debug("-------------------------------------------")
60
+ logger.debug("%s %s", request.method, request.url)
61
+ logger.debug("Headers: %s", request.headers)
62
+ logger.debug("Content: %s", request.content)
63
+ logger.debug("-------------------------------------------")
64
+
65
+ def _log_response(self, response: httpx.Response) -> None:
66
+ """Log every response to the console if debug is enabled."""
67
+
68
+ logger.debug("-------------------------------------------")
69
+ logger.debug("Response: %s", response.status_code)
70
+ logger.debug("Headers: %s", response.headers)
71
+ logger.debug("Content: %s", response.read())
72
+ logger.debug("--------------------------------------------")
73
+
74
+ def _get_error_message(self, response: httpx.Response) -> str:
75
+ """Get the error message from the response."""
76
+
77
+ try:
78
+ obj = response.json()
79
+ return f"{response.status_code}: {obj.get('detail', json.dumps(obj))}"
80
+ except Exception: # noqa: BLE001
81
+ return str(response.content)
82
+
83
+ def _request(
84
+ self,
85
+ method: str,
86
+ path: str,
87
+ params: dict[str, t.Any] | None = None,
88
+ json_data: dict[str, t.Any] | None = None,
89
+ ) -> httpx.Response:
90
+ """Make a raw request to the API."""
91
+
92
+ return self._client.request(method, path, json=json_data, params=params)
93
+
94
+ def request(
95
+ self,
96
+ method: str,
97
+ path: str,
98
+ params: dict[str, t.Any] | None = None,
99
+ json_data: dict[str, t.Any] | None = None,
100
+ ) -> httpx.Response:
101
+ """Make a request to the API. Raise an exception for non-200 status codes."""
102
+
103
+ response = self._request(method, path, params, json_data)
104
+ if response.status_code == 401: # noqa: PLR2004
105
+ raise RuntimeError("Authentication failed, please check your API token.")
106
+
107
+ try:
108
+ response.raise_for_status()
109
+ except httpx.HTTPStatusError as e:
110
+ raise RuntimeError(self._get_error_message(response)) from e
111
+
112
+ return response
113
+
114
+ # This currently won't work with API keys
115
+ # def get_user(self) -> UserResponse:
116
+ # response = self.request("GET", "/user")
117
+ # return UserResponse(**response.json())
118
+
119
+ def list_projects(self) -> list[Project]:
120
+ response = self.request("GET", "/strikes/projects")
121
+ return [Project(**project) for project in response.json()]
122
+
123
+ def get_project(self, project: str) -> Project:
124
+ response = self.request("GET", f"/strikes/projects/{project!s}")
125
+ return Project(**response.json())
126
+
127
+ def list_runs(self, project: str) -> list[Run]:
128
+ response = self.request("GET", f"/strikes/projects/{project!s}/runs")
129
+ return [Run(**run) for run in response.json()]
130
+
131
+ def get_run(self, run: str | ULID) -> Run:
132
+ response = self.request("GET", f"/strikes/projects/runs/{run!s}")
133
+ return Run(**response.json())
134
+
135
+ def get_run_tasks(self, run: str | ULID) -> list[Task]:
136
+ response = self.request("GET", f"/strikes/projects/runs/{run!s}/tasks")
137
+ return [Task(**task) for task in response.json()]
138
+
139
+ def get_run_trace(self, run: str | ULID) -> list[Task | TraceSpan]:
140
+ response = self.request("GET", f"/strikes/projects/runs/{run!s}/spans")
141
+ spans: list[Task | TraceSpan] = []
142
+ for item in response.json():
143
+ if "parent_task_span_id" in item:
144
+ spans.append(Task(**item))
145
+ else:
146
+ spans.append(TraceSpan(**item))
147
+ return spans
148
+
149
+ # Data exports
150
+
151
+ def export_runs(
152
+ self,
153
+ project: str,
154
+ *,
155
+ filter: str | None = None,
156
+ # format: ExportFormat = "parquet",
157
+ status: StatusFilter = "completed",
158
+ aggregations: list[MetricAggregationType] | None = None,
159
+ ) -> pd.DataFrame:
160
+ response = self.request(
161
+ "GET",
162
+ f"/strikes/projects/{project!s}/export",
163
+ params={
164
+ "format": "parquet",
165
+ "status": status,
166
+ **({"filter": filter} if filter else {}),
167
+ **({"aggregations": aggregations} if aggregations else {}),
168
+ },
169
+ )
170
+ return pd.read_parquet(io.BytesIO(response.content))
171
+
172
+ def export_metrics(
173
+ self,
174
+ project: str,
175
+ *,
176
+ filter: str | None = None,
177
+ # format: ExportFormat = "parquet",
178
+ status: StatusFilter = "completed",
179
+ metrics: list[str] | None = None,
180
+ aggregations: list[MetricAggregationType] | None = None,
181
+ ) -> pd.DataFrame:
182
+ response = self.request(
183
+ "GET",
184
+ f"/strikes/projects/{project!s}/export/metrics",
185
+ params={
186
+ "format": "parquet",
187
+ "status": status,
188
+ "filter": filter,
189
+ **({"metrics": metrics} if metrics else {}),
190
+ **({"aggregations": aggregations} if aggregations else {}),
191
+ },
192
+ )
193
+ return pd.read_parquet(io.BytesIO(response.content))
194
+
195
+ def export_parameters(
196
+ self,
197
+ project: str,
198
+ *,
199
+ filter: str | None = None,
200
+ # format: ExportFormat = "parquet",
201
+ status: StatusFilter = "completed",
202
+ parameters: list[str] | None = None,
203
+ metrics: list[str] | None = None,
204
+ aggregations: list[MetricAggregationType] | None = None,
205
+ ) -> pd.DataFrame:
206
+ response = self.request(
207
+ "GET",
208
+ f"/strikes/projects/{project!s}/export/parameters",
209
+ params={
210
+ "format": "parquet",
211
+ "status": status,
212
+ "filter": filter,
213
+ **({"parameters": parameters} if parameters else {}),
214
+ **({"metrics": metrics} if metrics else {}),
215
+ **({"aggregations": aggregations} if aggregations else {}),
216
+ },
217
+ )
218
+ return pd.read_parquet(io.BytesIO(response.content))
219
+
220
+ def export_timeseries(
221
+ self,
222
+ project: str,
223
+ *,
224
+ filter: str | None = None,
225
+ # format: ExportFormat = "parquet",
226
+ status: StatusFilter = "completed",
227
+ metrics: list[str] | None = None,
228
+ time_axis: TimeAxisType = "relative",
229
+ aggregations: list[TimeAggregationType] | None = None,
230
+ ) -> pd.DataFrame:
231
+ response = self.request(
232
+ "GET",
233
+ f"/strikes/projects/{project!s}/export/timeseries",
234
+ params={
235
+ "format": "parquet",
236
+ "status": status,
237
+ "filter": filter,
238
+ "time_axis": time_axis,
239
+ **({"metrics": metrics} if metrics else {}),
240
+ **({"aggregation": aggregations} if aggregations else {}),
241
+ },
242
+ )
243
+ return pd.read_parquet(io.BytesIO(response.content))
244
+
245
+ # User data access
246
+
247
+ def get_user_data_credentials(self) -> UserDataCredentials:
248
+ response = self.request("GET", "/user-data/credentials")
249
+ return UserDataCredentials(**response.json())