Skip to content

Commit b0d6332

Browse files
committed
Merge branch 'main' into mistralai-ai-usage
2 parents ef57aa8 + fe34e54 commit b0d6332

12 files changed

Lines changed: 127 additions & 9 deletions

File tree

.github/workflows/end2end.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
end2end-test:
2121
runs-on: ubuntu-latest
2222
continue-on-error: true
23+
timeout-minutes: 15
2324
needs: build
2425
strategy:
2526
matrix:

.github/workflows/unit-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ jobs:
66
test:
77
runs-on: ubuntu-latest
88
continue-on-error: true
9+
timeout-minutes: 15
910
strategy:
1011
# Don't cancel jobs if one fails
1112
fail-fast: false

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ Zen for Python 3 is compatible with:
5656

5757
### AI SDKs
5858
*[`openai`](https://pypi.org/project/openai) ^1.0
59+
*[`anthropic`](https://pypi.org/project/anthropic/)
5960
*[`mistralai`](https://pypi.org/project/mistralai) ^1.0.0
61+
6062
## Reporting to your Aikido Security dashboard
6163

6264
> Aikido is your no nonsense application security platform. One central system that scans your source code & cloud, shows you what vulnerabilities matter, and how to fix them - fast. So you can get back to building.

aikido_zen/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Aggregates from the different modules
33
"""
44

5+
import os
6+
57
# Re-export functions :
68
from aikido_zen.context.users import set_user
79
from aikido_zen.middleware import should_block_request
@@ -17,7 +19,7 @@
1719
from aikido_zen.helpers.aikido_disabled_flag_active import aikido_disabled_flag_active
1820

1921

20-
def protect(mode="daemon"):
22+
def protect(mode="daemon", token=""):
2123
"""
2224
Mode can be set to :
2325
- daemon : Default, imports sinks/sources and starts background_process
@@ -28,6 +30,9 @@ def protect(mode="daemon"):
2830
if aikido_disabled_flag_active():
2931
# Do not run any aikido code when the disabled flag is on
3032
return
33+
if token:
34+
os.environ["AIKIDO_TOKEN"] = token
35+
3136
if mode in ("daemon", "daemon_only"):
3237
start_background_process()
3338
if mode == "daemon_only":
@@ -68,6 +73,7 @@ def protect(mode="daemon"):
6873

6974
# Import AI sinks
7075
import aikido_zen.sinks.openai
76+
import aikido_zen.sinks.anthropic
7177
import aikido_zen.sinks.mistralai
7278

7379
logger.info("Zen by Aikido v%s starting.", PKG_VERSION)

aikido_zen/init_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import pytest
2+
3+
import aikido_zen
24
from aikido_zen import protect
35
from aikido_zen.background_process import get_comms, reset_comms
6+
from aikido_zen.helpers.token import get_token_from_env
47

58

69
def test_protect_with_django(monkeypatch, caplog):
@@ -9,3 +12,8 @@ def test_protect_with_django(monkeypatch, caplog):
912
assert "starting" in caplog.text
1013
reset_comms()
1114
assert get_comms() == None
15+
16+
17+
def test_protect_sets_token():
18+
aikido_zen.protect(token="MY_TOKEN_1")
19+
assert get_token_from_env().token == "MY_TOKEN_1"

aikido_zen/sinks/anthropic.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from aikido_zen.helpers.on_ai_call import on_ai_call
2+
from aikido_zen.helpers.register_call import register_call
3+
from aikido_zen.sinks import on_import, patch_function, after
4+
5+
6+
@after
7+
def _messages_create(func, instance, args, kwargs, return_value):
8+
op = f"anthropic.resources.messages.messages.Messages.create"
9+
register_call(op, "ai_op")
10+
11+
on_ai_call(
12+
provider="anthropic",
13+
model=return_value.model,
14+
input_tokens=return_value.usage.input_tokens,
15+
output_tokens=return_value.usage.output_tokens,
16+
)
17+
18+
19+
@on_import("anthropic.resources.messages")
20+
def patch(m):
21+
patch_function(m, "messages.Messages.create", _messages_create)

aikido_zen/sinks/builtins_import.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ def _import(func, instance, args, kwargs, return_value):
1414
return
1515
name = getattr(return_value, "__package__")
1616

17-
if not name or "." in name:
18-
# Make sure the name exists and that it's not a submodule
17+
if not name:
18+
# Make sure the name exists
1919
return
20-
if name == "importlib_metadata":
20+
name = name.split(".")[0] # Remove submodules
21+
if name == "importlib" or name == "importlib_metadata":
2122
# Avoid circular dependencies
2223
return
2324

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
3+
import pytest
4+
import aikido_zen.sinks.anthropic
5+
import anthropic
6+
7+
from aikido_zen.thread.thread_cache import get_cache
8+
9+
skip_no_api_key = pytest.mark.skipif(
10+
"ANTHROPIC_API_KEY" not in os.environ,
11+
reason="ANTHROPIC_API_KEY environment variable not set",
12+
)
13+
14+
15+
@pytest.fixture(autouse=True)
16+
def setup():
17+
get_cache().reset()
18+
yield
19+
get_cache().reset()
20+
21+
22+
def get_ai_stats():
23+
return get_cache().ai_stats.get_stats()
24+
25+
26+
@skip_no_api_key
27+
def test_anthropic_messages_create():
28+
client = anthropic.Anthropic()
29+
response = client.messages.create(
30+
model="claude-3-opus-20240229",
31+
max_tokens=20,
32+
messages=[
33+
{
34+
"role": "user",
35+
"content": "Write the longest response possible, just as I am writing a long content",
36+
}
37+
],
38+
)
39+
print(response)
40+
41+
assert get_ai_stats()[0]["model"] == "claude-3-opus-20240229"
42+
assert get_ai_stats()[0]["calls"] == 1
43+
assert get_ai_stats()[0]["provider"] == "anthropic"
44+
assert get_ai_stats()[0]["tokens"]["input"] == 21
45+
assert get_ai_stats()[0]["tokens"]["output"] == 20
46+
assert get_ai_stats()[0]["tokens"]["total"] == 41

end2end/django_mysql_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,6 @@ def test_initial_heartbeat():
8888
"method": "POST",
8989
"path": "/app/create"
9090
}],
91-
{"aborted":0,"attacksDetected":{"blocked":2,"total":2},"total":3}
91+
{"aborted":0,"attacksDetected":{"blocked":2,"total":2},"total":3},
92+
{'asgiref', 'regex', 'mysqlclient', 'sqlparse', 'aikido_zen', 'django'}
9293
)

end2end/server/check_events_from_mock.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ def validate_started_event(event, stack, dry_mode=False, serverless=False, os_na
2121
# if stack is not None:
2222
# assert set(event["agent"]["stack"]) == set(stack)
2323

24-
def validate_heartbeat(event, routes, req_stats):
24+
def validate_heartbeat(event, routes=None, req_stats=None, packages=None):
2525
assert event["type"] == "heartbeat", f"Expected event type 'heartbeat', but got '{event['type']}'"
26-
assert event["routes"] == routes, f"Expected routes '{routes}', but got '{event['routes']}'"
27-
assert event["stats"]["requests"] == req_stats, f"Expected request stats '{req_stats}', but got '{event['stats']['requests']}'"
26+
if packages:
27+
package_names = set(map(lambda x: x["name"], event["packages"]))
28+
assert package_names == packages, f"Expected {packages} but got {package_names}"
29+
if routes:
30+
assert event["routes"] == routes, f"Expected routes '{routes}', but got '{event['routes']}'"
31+
if req_stats:
32+
assert event["stats"]["requests"] == req_stats, f"Expected request stats '{req_stats}', but got '{event['stats']['requests']}'"
2833

0 commit comments

Comments
 (0)