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
2 changes: 2 additions & 0 deletions .github/instructions/python-lang.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@
- Always use f-strings for string interpolation. Ex: `f"User ID: {user_id}"` instead of `"User ID: {}".format(user_id)"`

- Never use `except:` without specifying the exception type. Always catch specific exceptions or use `except Exception as ex:` to capture the exception details. This also avoids accidentally catching system-exiting exceptions like `KeyboardInterrupt` or `SystemExit`.

- Never return raw exception text to browser or API clients. Do not use `str(e)`, `str(exc)`, `repr(e)`, traceback text, SDK exception messages, connection strings, or provider errors in `jsonify()`, template rendering, streaming responses, or other client-visible payloads. Log exception type and safe contextual metadata server-side with `log_event` or `debug_print`, then return a stable user-safe message such as `"Invalid request."`, `"Unable to save action."`, or `"Unable to store secrets in Key Vault."`

Check warning on line 48 in .github/instructions/python-lang.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 48 in .github/instructions/python-lang.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
34 changes: 34 additions & 0 deletions application/single_app/background_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
from functions_debug import debug_print
from functions_data_management import check_due_data_management_jobs_once
from functions_file_sync import check_due_file_sync_sources_once
from functions_keyvault_reminders import (

Check warning on line 38 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
KEY_VAULT_SECRET_REMINDER_LOCK_NAME,

Check warning on line 39 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
check_due_key_vault_secret_reminders_once,

Check warning on line 40 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
)
from functions_tabular_generated_exports import check_due_tabular_generated_output_runs_once
from functions_personal_workflows import (
compute_next_run_at,
Expand Down Expand Up @@ -782,6 +786,35 @@
time.sleep(max(int(sleep_seconds or 3600), 15))


def run_key_vault_secret_reminder_loop():

Check warning on line 789 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"""Run due Key Vault secret expiration reminder checks under a distributed lock."""

Check warning on line 790 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
while True:
lock_document = None
sleep_seconds = 21600
try:

Check warning on line 794 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
settings = get_settings()

Check warning on line 795 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
sleep_seconds = int(settings.get('key_vault_secret_expiration_scan_interval_seconds') or 21600)

Check warning on line 796 in application/single_app/background_tasks.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
if settings.get('enable_key_vault_secret_expiration_reminders'):
lock_document = acquire_distributed_task_lock(
KEY_VAULT_SECRET_REMINDER_LOCK_NAME,
lease_seconds=600,
)
if lock_document:
check_due_key_vault_secret_reminders_once(settings=settings)
except Exception as exc:
log_event(
'[KeyVaultReminders] Error in reminder scheduler loop.',
extra={'error': str(exc)},
level=logging.ERROR,
exceptionTraceback=True,
)
finally:
if lock_document:
release_distributed_task_lock(lock_document)

time.sleep(max(int(sleep_seconds or 21600), 900))


def start_background_task_threads(app=None):
"""Start all background task loops for the current process."""
task_specs = [
Expand All @@ -795,6 +828,7 @@
('Tabular generated-output scheduler background task started.', run_tabular_generated_output_scheduler_loop),
('Data Management scheduler background task started.', lambda: run_data_management_scheduler_loop(app=app)),
('App maintenance background task started.', run_app_maintenance_loop),
('Key Vault secret reminder background task started.', run_key_vault_secret_reminder_loop),
]

started_threads = []
Expand Down
8 changes: 7 additions & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.120"
VERSION = "0.250.124"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down Expand Up @@ -650,6 +650,12 @@ def _create_container_if_not_exists_with_conflict_recovery(*args, **kwargs):
partition_key=PartitionKey(path="/scope_key")
)

cosmos_key_vault_secret_reminders_container_name = "key_vault_secret_reminders"
cosmos_key_vault_secret_reminders_container = cosmos_database.create_container_if_not_exists(
id=cosmos_key_vault_secret_reminders_container_name,
partition_key=PartitionKey(path="/scope_key")
)

cosmos_personal_file_sync_sources_container_name = "personal_file_sync_sources"
cosmos_personal_file_sync_sources_container = cosmos_database.create_container_if_not_exists(
id=cosmos_personal_file_sync_sources_container_name,
Expand Down
123 changes: 123 additions & 0 deletions application/single_app/functions_appinsights.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import re
import threading
from datetime import date, datetime
from typing import Any, Dict, Optional, Tuple

from azure.monitor.opentelemetry import configure_azure_monitor
Expand Down Expand Up @@ -32,6 +33,14 @@
"subscriptionkey",
"token",
)
EXTERNAL_EVENT_SENSITIVE_KEY_FRAGMENTS = (
"email",
"userid",
"groupid",
"publicworkspaceid",
"scopevalue",
"sourceid",
)
SECRET_ASSIGNMENT_RE = re.compile(
r"(?i)\b(api[-_]?key|access[-_]?token|client[-_]?secret|connection[-_]?string|password|secret|subscription[-_]?key|token|sig|signature)=([^&\s,;]+)"
)
Expand Down Expand Up @@ -64,6 +73,8 @@
LOGGER_EVENT_MESSAGE = "[SimpleChatLogEvent]"
LOGGER_DEBUG_MESSAGE = "[SimpleChatDebugTrace]"
LOGGER_FALLBACK_MESSAGE = "[SimpleChatLogFallback]"
LOGGER_EXTERNAL_EVENT_MESSAGE = "[SimpleChatExternalEvent]"
MAX_EXTERNAL_EVENT_STRING_LENGTH = 256


def _format_message(message: Any, message_args: Optional[Tuple[Any, ...]] = None) -> str:
Expand All @@ -90,6 +101,18 @@ def _is_sensitive_log_key(key: Any) -> bool:
return any(fragment in normalized_key for fragment in SENSITIVE_LOG_KEY_FRAGMENTS)


def _is_sensitive_external_event_key(key: Any) -> bool:
normalized_key = _normalize_log_key(key)
if not normalized_key:
return False
if normalized_key.endswith("hash"):
return False
return (
_is_sensitive_log_key(key)
or any(fragment in normalized_key for fragment in EXTERNAL_EVENT_SENSITIVE_KEY_FRAGMENTS)
)


def sanitize_log_message(message: Any) -> str:
"""Redact secret-like values from log messages while preserving diagnostic text."""
message_text = str(message)
Expand Down Expand Up @@ -176,6 +199,61 @@ def _build_logger_extra(
return logger_extra


def _normalize_event_name(event_name: Any) -> str:
normalized_name = re.sub(r"[^A-Za-z0-9_.-]", "_", str(event_name or "").strip())[:100]
return normalized_name.strip("._-") or "simplechat_event"


def _normalize_external_event_key(key: Any) -> str:
normalized_key = re.sub(r"[^A-Za-z0-9_]", "_", str(key or "").strip())[:80]
normalized_key = normalized_key.strip("_") or "value"
return f"sc_event_{normalized_key}"


def _normalize_external_event_value(value: Any) -> Any:
if value is None:
return ""
if isinstance(value, bool) or isinstance(value, (int, float)):
return value
if isinstance(value, (date, datetime)):
return value.isoformat()
if isinstance(value, str):
return sanitize_log_message(value)[:MAX_EXTERNAL_EVENT_STRING_LENGTH]
if isinstance(value, dict):
return len(value)
if isinstance(value, (list, tuple, set)):
return len(value)
return type(value).__name__


def _build_external_event_extra(
event_name: str,
extra: Optional[Dict[str, Any]] = None,
allowed_sensitive_dimensions: Optional[Tuple[str, ...]] = None,
) -> Dict[str, Any]:
event_extra: Dict[str, Any] = {
"sc_event_name": event_name,
}
allowed_sensitive_keys = {
_normalize_log_key(key)
for key in (allowed_sensitive_dimensions or ())
if _normalize_log_key(key)
}

if isinstance(extra, dict):
for key, value in extra.items():
normalized_key = _normalize_external_event_key(key)
if (
_is_sensitive_external_event_key(key)
and _normalize_log_key(key) not in allowed_sensitive_keys
):
event_extra[f"{normalized_key}_present"] = value is not None
continue
event_extra[normalized_key] = _normalize_external_event_value(value)

return event_extra


def _load_logging_settings() -> Dict[str, Any]:
"""Read cached settings first and fall back to live settings when needed."""
if getattr(_logging_settings_load_state, 'active', False):
Expand Down Expand Up @@ -398,6 +476,51 @@ def log_event(
if safe_extra:
print("[LOG] Extra dimensions were redacted for logging safety.")


def log_external_event(
event_name: str,
extra: Optional[Dict[str, Any]] = None,
level: int = logging.INFO,
allowed_sensitive_dimensions: Optional[Tuple[str, ...]] = None,
) -> None:
"""
Emit a privacy-safe, queryable telemetry event for Azure Monitor automation.

Unlike general-purpose log_event records, this helper preserves explicitly
safe string dimensions so scheduled query alerts and downstream automation
can filter by event name and categorical fields.
"""
normalized_event_name = _normalize_event_name(event_name)
event_extra = _build_external_event_extra(
normalized_event_name,
extra,
allowed_sensitive_dimensions=allowed_sensitive_dimensions,
)

try:
logger = get_appinsights_logger()
if not logger:
logger = logging.getLogger('standard')
if not logger.handlers:
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)

logger.log(
level,
LOGGER_EXTERNAL_EVENT_MESSAGE,
extra=event_extra,
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
stacklevel=2,
)
except Exception as exc:
log_event(
"[AppInsights] Failed to emit external telemetry event.",
extra={
"event_name": normalized_event_name,
"error_type": type(exc).__name__,
},
level=logging.ERROR,
)

# --- Modern Azure Monitor Application Insights setup ---
def setup_appinsights_logging(settings):
"""
Expand Down
3 changes: 1 addition & 2 deletions application/single_app/functions_global_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def save_global_action(action_data, user_id=None):
except Exception as e:
print(f"❌ Error saving global action: {str(e)}")
traceback.print_exc()
return None
raise


def delete_global_action(action_id):
Expand Down Expand Up @@ -229,4 +229,3 @@ def update_global_action_enabled(action_id, is_enabled, user_id=None):
print(f"❌ Error updating enabled state for global action {action_id}: {str(e)}")
traceback.print_exc()
return None

Loading
Loading