-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Azure Function App which runs periodically to aggregate the bytes rea…
…d per IP address over a window of time (#215) * init * update * update flake8 config * code format changes * fix long lines * fix imports * function app changes * code format changes * test * remove readme * remove redundant packages and revert python version * revert python version * remove redundant type hints * use settings class inherited from baseSettings * change kql * use placeholder for timer schedule * change test parameter * update assertions and logger * remove import * remove dash in table name * update test id * format * add no-integration flag * add dependencies * test * change trigger to run every hour * use azure clients as context manager * add context managers in test * role assignment for function app * change LAW name * change role * change provider's name * change name of LAW * better readability * better readability * format * add logging * UPDATE function settings * suppress mypy warning * update env variables for function app * typo --------- Co-authored-by: elay <[email protected]>
- Loading branch information
Showing
19 changed files
with
386 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,5 @@ extend-ignore = E203, W503 | |
exclude = | ||
.git | ||
__pycache__ | ||
setup.py | ||
setup.py | ||
.venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import datetime | ||
import logging | ||
|
||
import azure.functions as func | ||
from azure.data.tables import TableServiceClient | ||
from azure.identity import DefaultAzureCredential | ||
from azure.monitor.query import LogsQueryClient | ||
|
||
from .config import settings | ||
from .models import UpdateBannedIPTask | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def main(mytimer: func.TimerRequest) -> None: | ||
utc_timestamp: str = ( | ||
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() | ||
) | ||
logger.info("Updating the ip ban list at %s", utc_timestamp) | ||
credential: DefaultAzureCredential = DefaultAzureCredential() | ||
with LogsQueryClient(credential) as logs_query_client: | ||
with TableServiceClient( | ||
endpoint=settings.storage_account_url, credential=credential | ||
) as table_service_client: | ||
with table_service_client.create_table_if_not_exists( | ||
settings.banned_ip_table | ||
) as table_client: | ||
task = UpdateBannedIPTask(logs_query_client, table_client) | ||
task.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# config.py | ||
from pydantic import BaseSettings, Field | ||
|
||
|
||
class Settings(BaseSettings): | ||
storage_account_url: str = Field(env="STORAGE_ACCOUNT_URL") | ||
banned_ip_table: str = Field(env="BANNED_IP_TABLE") | ||
log_analytics_workspace_id: str = Field(env="LOG_ANALYTICS_WORKSPACE_ID") | ||
|
||
# Time and threshold settings | ||
time_window_in_hours: int = Field(default=24, env="TIME_WINDOW_IN_HOURS") | ||
threshold_read_count_in_gb: int = Field( | ||
default=5120, env="THRESHOLD_READ_COUNT_IN_GB" | ||
) | ||
|
||
|
||
# Create a global settings instance | ||
settings = Settings() # type: ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"scriptFile": "__init__.py", | ||
"bindings": [ | ||
{ | ||
"name": "mytimer", | ||
"type": "timerTrigger", | ||
"direction": "in", | ||
"schedule": "0 */1 * * *" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import logging | ||
from typing import Any, List, Set | ||
|
||
from azure.data.tables import TableClient, UpdateMode | ||
from azure.monitor.query import LogsQueryClient | ||
from azure.monitor.query._models import LogsTableRow | ||
|
||
from .config import settings | ||
|
||
|
||
class UpdateBannedIPTask: | ||
def __init__( | ||
self, | ||
logs_query_client: LogsQueryClient, | ||
table_client: TableClient, | ||
) -> None: | ||
self.log_query_client = logs_query_client | ||
self.table_client = table_client | ||
|
||
def run(self) -> List[LogsTableRow]: | ||
query_result: List[LogsTableRow] = self.get_blob_logs_query_result() | ||
logging.info(f"Kusto query result: {query_result}") | ||
self.update_banned_ips(query_result) | ||
return query_result | ||
|
||
def get_blob_logs_query_result(self) -> List[LogsTableRow]: | ||
query: str = f""" | ||
StorageBlobLogs | ||
| where TimeGenerated > ago({settings.time_window_in_hours}h) | ||
| extend IpAddress = tostring(split(CallerIpAddress, ":")[0]) | ||
| where OperationName == 'GetBlob' | ||
| where not(ipv4_is_private(IpAddress)) | ||
| summarize readcount = sum(ResponseBodySize) / (1024 * 1024 * 1024) | ||
by IpAddress | ||
| where readcount > {settings.threshold_read_count_in_gb} | ||
""" | ||
response: Any = self.log_query_client.query_workspace( | ||
settings.log_analytics_workspace_id, query, timespan=None | ||
) | ||
return response.tables[0].rows | ||
|
||
def update_banned_ips(self, query_result: List[LogsTableRow]) -> None: | ||
existing_ips = { | ||
entity["RowKey"] for entity in self.table_client.list_entities() | ||
} | ||
result_ips: Set[str] = set() | ||
for result in query_result: | ||
ip_address: str = result[0] | ||
read_count: int = int(result[1]) | ||
result_ips.add(ip_address) | ||
entity = { | ||
"PartitionKey": ip_address, | ||
"RowKey": ip_address, | ||
"ReadCount": read_count, | ||
"Threshold": settings.threshold_read_count_in_gb, | ||
"TimeWindow": settings.time_window_in_hours, | ||
} | ||
|
||
if ip_address in existing_ips: | ||
self.table_client.update_entity(entity, mode=UpdateMode.REPLACE) | ||
else: | ||
self.table_client.create_entity(entity) | ||
|
||
for ip_address in existing_ips: | ||
if ip_address not in result_ips: | ||
self.table_client.delete_entity( | ||
partition_key=ip_address, row_key=ip_address | ||
) | ||
|
||
logging.info("IP ban list has been updated successfully") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from typing import List | ||
|
||
import pytest | ||
|
||
|
||
def pytest_addoption(parser: pytest.Parser) -> None: | ||
parser.addoption( | ||
"--no-integration", | ||
action="store_true", | ||
default=False, | ||
help="don't run integration tests", | ||
) | ||
|
||
|
||
def pytest_configure(config: pytest.Config) -> None: | ||
config.addinivalue_line("markers", "integration: mark as an integration test") | ||
|
||
|
||
def pytest_collection_modifyitems( | ||
config: pytest.Config, items: List[pytest.Item] | ||
) -> None: | ||
if config.getoption("--no-integration"): | ||
# --no-integration given in cli: skip integration tests | ||
skip_integration = pytest.mark.skip( | ||
reason="needs --no-integration option to run" | ||
) | ||
for item in items: | ||
if "integration" in item.keywords: | ||
item.add_marker(skip_integration) |
Empty file.
Oops, something went wrong.