Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ all: format lint

-include .env

GS_REST_SERVICE_VERSIONM ?= "25.11.4"
GS_REST_SERVICE_VERSION ?= "1.15.4"
GS_REST_SERVICE_VERSIONM ?= "25.12.0rc2"
GS_REST_SERVICE_VERSION ?= "1.16.0rc2"

GS_REST_DEV_PORT ?= 9000
NUM_WORKERS ?= 1
Expand Down
4 changes: 2 additions & 2 deletions clients/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ GraphSense API provides programmatic access to various ledgers' addresses, entit

This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:

- API version: 1.15.4
- Package version: 1.15.4
- API version: 1.16.0rc2
- Package version: 1.16.0rc2
- Build package: org.openapitools.codegen.languages.PythonClientCodegen

## Requirements.
Expand Down
2 changes: 1 addition & 1 deletion clients/python/graphsense/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""


__version__ = "1.15.4"
__version__ = "1.16.0rc2"

# import ApiClient
from graphsense.api_client import ApiClient
Expand Down
2 changes: 1 addition & 1 deletion clients/python/graphsense/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None,
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'OpenAPI-Generator/1.15.4/python'
self.user_agent = 'OpenAPI-Generator/1.16.0rc2/python'

def __enter__(self):
return self
Expand Down
4 changes: 2 additions & 2 deletions clients/python/graphsense/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@ def to_debug_report(self):
return "Python SDK Debug Report:\n"\
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: 1.15.4\n"\
"SDK Package Version: 1.15.4".\
"Version of the API: 1.16.0rc2\n"\
"SDK Package Version: 1.16.0rc2".\
format(env=sys.platform, pyversion=sys.version)

def get_host_settings(self):
Expand Down
2 changes: 1 addition & 1 deletion clients/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "graphsense-python"
version = "1.15.4"
version = "1.16.0rc2"
description = "GraphSense API"
readme = { file = "README.md", content-type = "text/markdown; charset=UTF-8; variant=GFM" }
requires-python = ">=3.6"
Expand Down
18 changes: 17 additions & 1 deletion gsrest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,28 @@ def factory_internal(

# Initialize service container after db connection is established
async def setup_services(app):
config = app["config"]
if config.tag_access_logger and config.tag_access_logger.enabled:
app.logger.info("Tag access logging is enabled.")
from redis import asyncio as aioredis

redis_url = config.tag_access_logger.redis_url or "redis://localhost"
app.logger.info(
f"Connecting to Redis at {redis_url} for tag access logging."
)
redis_client = await aioredis.from_url(redis_url)
log_tag_access_prefix = config.tag_access_logger.prefix
else:
redis_client = None
log_tag_access_prefix = None
app["services"] = ServiceContainer(
config=app["config"],
config=config,
db=app["db"],
tagstore_engine=app["gs-tagstore"],
concepts_cache_service=ConceptsCacheService(app, app.logger),
logger=app.logger,
redis_client=redis_client,
log_tag_access_prefix=log_tag_access_prefix,
)
yield

Expand Down
2 changes: 1 addition & 1 deletion gsrest/builtin/plugins/obfuscate_tags/obfuscate_tags.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from functools import partial

from aiohttp import web
from graphsenselib.tagstore.algorithms.obfuscate import (
Expand All @@ -18,7 +19,6 @@
from openapi_server.models.search_result_level4 import SearchResultLevel4
from openapi_server.models.search_result_level5 import SearchResultLevel5
from openapi_server.models.search_result_level6 import SearchResultLevel6
from functools import partial

GROUPS_HEADER_NAME = "X-Consumer-Groups"
NO_OBFUSCATION_MARKER_PATTERN = re.compile(r"(private|tags-private)")
Expand Down
15 changes: 15 additions & 0 deletions gsrest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ class LoggingConfig(BaseSettings):
)


class TagAccessLoggerConfig(BaseSettings):
enabled: bool = Field(default=False, description="Enable tag access logging")
prefix: str = Field(
default="tag_access",
description="Prefix for Redis keys used in tag access logging",
)
redis_url: Optional[str] = Field(
default=None, description="Redis URL for tag access logging"
)


class GSRestConfig(BaseSettings):
model_config = ConfigDict(env_prefix="GSREST_", case_sensitive=False, extra="allow")

Expand Down Expand Up @@ -92,6 +103,10 @@ class GSRestConfig(BaseSettings):
default_factory=dict, description="Slack info hook"
)

tag_access_logger: Optional[TagAccessLoggerConfig] = Field(
default=None, description="Tag access logger configuration"
)

def get_plugin_config(self, plugin_name: str) -> Optional[Dict[str, Any]]:
"""Get configuration for a specific plugin"""
return getattr(self, plugin_name, None)
Expand Down
85 changes: 83 additions & 2 deletions gsrest/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any, Optional
import time
from typing import Any, Optional, Tuple

from graphsenselib.db.asynchronous.services.addresses_service import AddressesService
from graphsenselib.db.asynchronous.services.blocks_service import BlocksService
Expand All @@ -13,6 +14,7 @@
from graphsenselib.db.asynchronous.services.tokens_service import TokensService
from graphsenselib.db.asynchronous.services.txs_service import TxsService
from graphsenselib.tagstore.db import TagstoreDbAsync, Taxonomies
from graphsenselib.tagstore.db.queries import TagPublic

from gsrest.builtin.plugins.obfuscate_tags.obfuscate_tags import (
GROUPS_HEADER_NAME,
Expand Down Expand Up @@ -47,6 +49,78 @@ async def setup_cache(cls, db_engine: Any, app: Any):
}


class TagAccessLoggerTagstoreProxy:
"""Adds logging for which tags are accessed from the tagstore
it intercepts calls to the tagstore DB
and logs returned tags to redis.
"""

def __init__(
self, tagstore_db: TagstoreDbAsync, redis_client: Any, key_prefix: str
):
self.tagstore_db = tagstore_db
self.redis_client = redis_client
self.key_prefix = key_prefix

def __getattr__(self, name):
"""Proxy all method calls to the underlying tagstore_db"""
attr = getattr(self.tagstore_db, name)

if callable(attr):

async def wrapper(*args, **kwargs):
# Call the original method
result = await attr(*args, **kwargs)

# Log tag access if this method returns TagPublic objects
should_log, is_list = self._should_log_result(result)
if self.redis_client and should_log:
if is_list:
for tag in result:
await self._log_tag_access(name, tag, *args, **kwargs)
else:
await self._log_tag_access(name, result, *args, **kwargs)

return result

return wrapper
else:
return attr

def _should_log_result(self, result: Any) -> Tuple[bool, bool]:
"""Determine if this result should be logged based on data type"""

if not result:
return False, False

# Check if result is a PublicTag
if isinstance(result, TagPublic):
return True, False

# Check if result is a list of TagPublic objects
if hasattr(result, "__iter__") and not isinstance(result, str):
try:
# Check if all items in the iterable are TagPublic objects
for item in result:
if isinstance(item, TagPublic):
return True, True
break # Only check first item for performance
except (TypeError, StopIteration):
pass

return False, False

async def _log_tag_access(self, method_name: str, tag: TagPublic, *args, **kwargs):
"""Log tag access information to Redis"""

current_time = time.localtime()
timestamp = time.strftime("%Y-%m-%d", current_time)
key = "|".join(
(self.key_prefix, timestamp, tag.creator, tag.network, tag.identifier)
)
await self.redis_client.incr(key)


class ServiceContainer:
def __init__(
self,
Expand All @@ -55,10 +129,17 @@ def __init__(
tagstore_engine: any,
concepts_cache_service: ConceptsCacheService,
logger: any,
redis_client: Optional[Any] = None,
log_tag_access_prefix: Optional[str] = None,
):
tsdb = TagstoreDbAsync(tagstore_engine)
self.config = config
self.db = db
self.tagstore_db = TagstoreDbAsync(tagstore_engine)
self.tagstore_db = (
TagAccessLoggerTagstoreProxy(tsdb, redis_client, log_tag_access_prefix)
if log_tag_access_prefix
else tsdb
)
self.logger = logger
self.category_cache_service = concepts_cache_service

Expand Down
4 changes: 2 additions & 2 deletions gsrest/service/general_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from graphsenselib.db.asynchronous.services.models import SearchRequestConfig

from gsrest.dependencies import get_service_container, get_tagstore_access_groups
from gsrest.translators import (
pydantic_search_result_by_currency_to_openapi,
pydantic_search_result_to_openapi,
pydantic_stats_to_openapi,
)

from graphsenselib.db.asynchronous.services.models import SearchRequestConfig


async def get_statistics(request):
"""Returns summary statistics on all available currencies"""
Expand Down
2 changes: 1 addition & 1 deletion openapi_server/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ info:
name: Iknaio Cryptoasset Analytics GmbH
description: GraphSense API provides programmatic access to various ledgers' addresses, entities, blocks, transactions and tags for automated and highly efficient forensics tasks.
title: GraphSense API
version: 1.15.4
version: 1.16.0rc2
servers:
- url: https://api.ikna.io
paths:
Expand Down
2 changes: 1 addition & 1 deletion openapi_spec/graphsense.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ info:
contact:
name: Iknaio Cryptoasset Analytics GmbH
email: contact@ikna.io
version: "1.15.4"
version: "1.16.0rc2"
servers:
- url: 'https://api.ikna.io'
paths:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "gsrest"
version = "1.15.4"
version = "1.16.0rc2"
description = "GraphSense API REST API"
readme = "README.md"
keywords = ["OpenAPI", "GraphSense API"]
Expand All @@ -19,6 +19,7 @@ dependencies = [
"graphsense-lib[conversions,tagpacks]>=2.8.9",
"openapi-schema-validator>=0.2.3",
"python-dateutil>=2.9.0",
"redis>=7.1.0",
"swagger-ui-bundle>=0.0.9",
]

Expand Down
Loading