Skip to content

Commit 4996469

Browse files
committed
Restore old function solution to create a time buffer for customers to switch
1 parent fd160f2 commit 4996469

25 files changed

Lines changed: 1841 additions & 0 deletions

Solutions/WithSecureElementsViaFunction/Data Connectors/WithSecureElementsAzureFunction/__init__.py

Whitespace-only changes.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import logging
2+
import os
3+
4+
import azure.functions as func
5+
import requests
6+
from azure.data.tables import TableServiceClient
7+
from azure.identity import ClientSecretCredential
8+
from azure.monitor.ingestion import LogsIngestionClient
9+
10+
from lib.azure_storage_table import StorageTableClient
11+
from lib.log_ingestion_api import IngestionApiClient
12+
from lib.withsecure_client import WithSecureClient
13+
from lib.ws_connector import Connector
14+
15+
app = func.FunctionApp()
16+
17+
API_URL = "ELEMENTS_API_URL"
18+
ENGINE = "ENGINE"
19+
ENGINE_GROUP = "ENGINE_GROUP"
20+
21+
CLIENT_ID_KEY = "ELEMENTS_API_CLIENT_ID"
22+
CLIENT_SECRET_KEY = "ELEMENTS_API_CLIENT_SECRET"
23+
ENTRA_ID_KEY = "ENTRA_CLIENT_ID"
24+
ENTRA_SECRET_KEY = "ENTRA_CLIENT_SECRET"
25+
ENTRA_TENANT_KEY = "ENTRA_TENANT_ID"
26+
27+
CONNECTION_STRING = "STATE_TABLE_CS"
28+
TABLE_NAME = "STATE_TABLE"
29+
DATA_COLLECTION_ENDPOINT = "LOGS_ENDPOINT"
30+
DCR_RULE_ID = "LOGS_DCR_RULE_ID"
31+
DCR_STREAM = "LOGS_DCR_STREAM_NAME"
32+
33+
MAX_EVENTS_COUNT = "MAX_EVENTS_COUNT"
34+
35+
log = logging.getLogger(__name__)
36+
37+
38+
@app.schedule(
39+
schedule="0 * * * * *", arg_name="timer", run_on_startup=True, use_monitor=True
40+
)
41+
def upload_security_events(timer):
42+
if timer.past_due:
43+
log.info("The timer is past due!")
44+
try:
45+
elements_api_url = os.environ.get(API_URL)
46+
state_conn_str = os.environ.get(CONNECTION_STRING)
47+
state_table = os.environ.get(TABLE_NAME)
48+
logs_endpoint = os.environ.get(DATA_COLLECTION_ENDPOINT)
49+
dcr_rule_id = os.environ.get(DCR_RULE_ID)
50+
dcr_stream = os.environ.get(DCR_STREAM)
51+
client_id = os.environ.get(CLIENT_ID_KEY)
52+
client_secret = os.environ.get(CLIENT_SECRET_KEY)
53+
entra_client = os.environ.get(ENTRA_ID_KEY)
54+
entra_secret = os.environ.get(ENTRA_SECRET_KEY)
55+
entra_tenant = os.environ.get(ENTRA_TENANT_KEY)
56+
max_events_count = os.environ.get(MAX_EVENTS_COUNT, 1000)
57+
58+
log.info("Secret values ok " + client_id)
59+
60+
storage_client = storage_table_client(state_conn_str, state_table)
61+
62+
ingestion_client = ingestion_api_client(
63+
logs_endpoint,
64+
dcr_rule_id,
65+
dcr_stream,
66+
entra_tenant,
67+
entra_client,
68+
entra_secret,
69+
)
70+
71+
engine = os.environ.get(ENGINE, "default")
72+
engine_group = os.environ.get(ENGINE_GROUP, "default")
73+
withsecure_client = WithSecureClient(
74+
elements_api_url, client_id, client_secret, engine, engine_group, requests, max_events_count
75+
)
76+
77+
connector = Connector(
78+
storage_client=storage_client,
79+
ingestion_client=ingestion_client,
80+
withsecure_client=withsecure_client,
81+
)
82+
83+
connector.execute()
84+
except Exception:
85+
log.exception("Execution error")
86+
87+
88+
def storage_table_client(connection_str, table_name):
89+
table_service_client = TableServiceClient.from_connection_string(connection_str)
90+
return StorageTableClient(table_service_client.get_table_client(table_name))
91+
92+
93+
def ingestion_api_client(
94+
api_url, dcr_rule_id, dcr_stream, entra_tenant, entra_client, entra_secret
95+
):
96+
creds = ClientSecretCredential(
97+
tenant_id=entra_tenant, client_id=entra_client, client_secret=entra_secret
98+
)
99+
azure_client = LogsIngestionClient(api_url, credential=creds)
100+
return IngestionApiClient(azure_client, dcr_rule_id, dcr_stream)

Solutions/WithSecureElementsViaFunction/Data Connectors/WithSecureElementsAzureFunction/lib/__init__.py

Whitespace-only changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
from datetime import datetime, timedelta, timezone
3+
4+
from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
5+
from azure.data.tables import UpdateMode
6+
7+
log = logging.getLogger(__name__)
8+
9+
10+
class StorageTableClient:
11+
def __init__(self, table_client):
12+
self._table_client = table_client
13+
self._partition_key = "WithSecureConnectorViaAPI"
14+
self._row_key = "LastSuccessfulRead"
15+
16+
def get_start_timestamp(self):
17+
last_timestamp = self._get_timestamp()
18+
if last_timestamp is None:
19+
last_minute_timestamp = datetime.now(timezone.utc) - timedelta(minutes=1)
20+
last_timestamp = last_minute_timestamp.isoformat(
21+
sep="T", timespec="milliseconds"
22+
).replace("+00:00", "Z")
23+
log.info(
24+
f"Timestamp missing from storage, generating with {last_timestamp}"
25+
)
26+
self._add_timestamp_entity(last_timestamp)
27+
return last_timestamp
28+
29+
def save_start_timestamp(self, timestamp):
30+
self._update_timestamp(timestamp)
31+
32+
def _add_timestamp_entity(self, timestamp):
33+
try:
34+
entity = self._timestamp_to_entity(timestamp)
35+
resp = self._table_client.create_entity(entity)
36+
log.info(f"Entity created: {resp}")
37+
except ResourceExistsError:
38+
log.info("Entity already exist")
39+
40+
def _get_timestamp(self):
41+
try:
42+
return self._table_client.get_entity(self._partition_key, self._row_key)[
43+
self._row_key
44+
]
45+
except ResourceNotFoundError:
46+
return None
47+
48+
def _update_timestamp(self, timestamp):
49+
entity = self._timestamp_to_entity(timestamp)
50+
self._table_client.update_entity(entity, UpdateMode.REPLACE)
51+
52+
def _timestamp_to_entity(self, timestamp):
53+
return {
54+
"PartitionKey": self._partition_key,
55+
"RowKey": self._row_key,
56+
self._row_key: timestamp,
57+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import re
2+
3+
4+
class Formatter:
5+
SEVERITY_TO_INDEX = {
6+
"critical": 10,
7+
"fatal_error": 8,
8+
"error": 6,
9+
"warning": 4,
10+
"info": 1,
11+
}
12+
ENGINE_TO_ACTIVITY = {
13+
"webTrafficScanning": "Web Traffic Scanning event",
14+
"webContentControl": "Web Content Control event",
15+
"applicationControl": "Application Control event",
16+
"fileScanning": "File Scanning event",
17+
"manualScanning": "Manual Scanning event",
18+
"realtimeScanning": "Real - time Scanning event",
19+
"deviceControl": "Device Control event",
20+
"deepGuard": "DeepGuard event",
21+
"dataGuard": "DataGuard event",
22+
"browsingProtection": "Browsing Protection event",
23+
"connectionControl": "Network connection event",
24+
"reputationBasedBrowsing": "Browsing Protection event",
25+
"integrityChecker": "Integrity Checker event",
26+
"tamperProtection": "Tamper Protection event",
27+
"firewall": "Firewall event",
28+
"amsi": "Antimalware Scan Interface (AMSI) event",
29+
"connector": "Elements Connector event",
30+
"setting": "Setting event",
31+
"edr": "Endpoint Detection and Response (EDR) event",
32+
"xFence": "XFENCE event",
33+
"systemEventsLog": "System event",
34+
"activityMonitor": "Server Share Protection event",
35+
"emailScan": "Email scanning event",
36+
"emailBreach": "Email breach event",
37+
"teamsScan": "Microsoft Teams scanning event",
38+
"oneDriveScan": "Microsoft OneDrive scanning event",
39+
"sharePointScan": "Microsoft SharePoint scanning event",
40+
"inboxRuleScan": "Suspicious inbox rule event",
41+
"activityMonitorClientProtection": "Activity Monitor Client Protection",
42+
}
43+
DEEP_GUARD_RARITY_REPUTATION_REGEX = "Rarity: (\\d+), Reputation: (\\d+)"
44+
45+
def format(self, event):
46+
engine = event.engine
47+
result = {
48+
"DeviceVendor": "WithSecure",
49+
"DeviceEventClassID": f"{engine}.{event.action}",
50+
"Activity": Formatter.ENGINE_TO_ACTIVITY.get(engine, engine),
51+
"LogSeverity": Formatter.SEVERITY_TO_INDEX.get(event.severity),
52+
"Message": event.message,
53+
"DeviceAction": event.action,
54+
"SimplifiedDeviceAction": event.action,
55+
"PersistenceTimestamp": event.persistenceTimestamp,
56+
}
57+
details = event.details
58+
infected_object = Formatter.get_infection_properties(engine, event)
59+
if "infectedObject" in infected_object:
60+
result["DeviceCustomString1"] = infected_object["infectedObject"]
61+
result["DeviceCustomString1Label"] = "Infected object"
62+
if "malwareName" in infected_object:
63+
result["DeviceCustomString2"] = infected_object["malwareName"]
64+
result["DeviceCustomString2Label"] = "Malware"
65+
if "userName" in details:
66+
result["SourceUserName"] = details["userName"]
67+
68+
host_name = event.host_name()
69+
if host_name:
70+
result["SourceHostName"] = host_name
71+
additional_extensions = dict(details)
72+
additional_extensions["accountName"] = event.organization["name"]
73+
additional_extensions_string = ";".join(
74+
[f"details_{k}={v}" for k, v in additional_extensions.items()]
75+
)
76+
result["AdditionalExtensions"] = additional_extensions_string
77+
return result
78+
79+
@staticmethod
80+
def get_infection_properties(engine, event):
81+
details = event.details
82+
match engine:
83+
case "fileScanning" | "manualScanning":
84+
return Formatter.get_infection_object(
85+
event.file_path(), event.infection_name()
86+
)
87+
case "deepGuard":
88+
name = details.get("name", "")
89+
match_result = re.findall(
90+
Formatter.DEEP_GUARD_RARITY_REPUTATION_REGEX, name
91+
)
92+
if not match_result and name != "DeepGuard blocks a rare application":
93+
return Formatter.get_infection_object(
94+
details.get("filePath", ""), details.get("name", "")
95+
)
96+
case "webTrafficScanning":
97+
if "alertType" in details:
98+
return Formatter.get_infection_object(
99+
details.get("url", ""),
100+
event.process_name(),
101+
)
102+
else:
103+
return Formatter.get_infection_object(
104+
details.get("websiteUrl", ""), event.file_name()
105+
)
106+
case "amsi":
107+
return Formatter.get_infection_object(
108+
details.get("path", ""), details.get("infectionName", "")
109+
)
110+
case _:
111+
return {}
112+
return {}
113+
114+
@staticmethod
115+
def get_infection_object(path, name):
116+
return {"infectedObject": path, "malwareName": name}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import logging
2+
3+
from azure.core.exceptions import HttpResponseError
4+
5+
log = logging.getLogger(__name__)
6+
7+
8+
class IngestionApiClient:
9+
def __init__(self, azure_ingestion_client, dcr_rule_id, dcr_stream):
10+
self._ingestion_client = azure_ingestion_client
11+
self._dcr_rule_id = dcr_rule_id
12+
self._dcr_stream = dcr_stream
13+
14+
def upload_events(self, events):
15+
log.info("Uploading events to Log Workspace")
16+
try:
17+
self._ingestion_client.upload(
18+
rule_id=self._dcr_rule_id, stream_name=self._dcr_stream, logs=events
19+
)
20+
log.info("Events are uploaded")
21+
except HttpResponseError as e:
22+
raise Exception("Couldn't send data to Azure") from e

0 commit comments

Comments
 (0)