Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.293"
VERSION = "0.261.294"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
36 changes: 36 additions & 0 deletions application/single_app/functions_action_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@
MCP_TYPE_ALIASES = frozenset({
"mcp", "mcpplugin", "modelcontextprotocol", "modelcontextprotocolplugin",
})
# Action types that run with the application identity against customer data and are
# therefore created only by administrators as global actions; users reach them through agents.

Check warning on line 18 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
GLOBAL_ONLY_ACTION_TYPES = frozenset({"azure_files_index"})
# Plugin class names behind GLOBAL_ONLY_ACTION_TYPES, keyed the way the Semantic Kernel loaders

Check warning on line 20 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
# match an action type to a plugin class (see _loader_type_key).

Check warning on line 21 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
_GLOBAL_ONLY_PLUGIN_CLASS_KEYS = frozenset({"azurefilesindexplugin"})

Check warning on line 22 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.


class GlobalOnlyActionTypeError(PermissionError):

Check warning on line 25 in application/single_app/functions_action_manifest.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.
"""An action type that only administrators may create, as a global action."""

code = "global_only_action_type"
public_message = "This action type is available only as an administrator-managed global action."

def __init__(self):
super().__init__(self.public_message)


def _loader_type_key(action_type):
"""Normalize a type exactly as the plugin loaders do before matching plugin class names."""

Check warning on line 36 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
text = re.sub(r"\s", "", str(action_type or ""))
return text.replace("_", "").replace("-", "").replace("plugin", "").lower()

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

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.


def is_global_only_action_type(action_type):
"""Return whether an action type may be created only as a global action.

The plugin loaders run an action with the first plugin class whose normalized name contains

Check warning on line 44 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
the normalized type, so aliases such as ``AzureFilesIndex`` or ``files_index`` reach a
global-only plugin class as well. A non-empty type that normalizes to nothing matches every

Check warning on line 46 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 46 in application/single_app/functions_action_manifest.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains dynamic execution, persistence, or system access marker. Recommendation%3A Do not execute changed lifecycle scripts or installers while this finding is unresolved.
class and is treated the same way.
"""
if not str(action_type or "").strip():
return False
key = _loader_type_key(action_type)
return not key or any(key in class_key for class_key in _GLOBAL_ONLY_PLUGIN_CLASS_KEYS)


class McpConfigurationError(ValueError):
Expand Down
56 changes: 56 additions & 0 deletions application/single_app/functions_activity_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2134,6 +2134,62 @@ def log_file_sync_activity(
debug_print(f"Warning: Failed to log File Sync activity: {str(e)}")


def log_azure_files_search_access(user_id: str, review: Dict[str, Any]) -> None:
"""Record an Azure Files Search that withheld denied or unverifiable files.

The review holds counts, reason codes, and a bounded list of withheld file paths. It never
holds file content or the user's query, and only administrators can read activity logs.
"""
normalized_user_id = coerce_activity_log_user_id(user_id)
now = datetime.utcnow().isoformat()
counts = (review or {}).get('counts') or {}
withheld = int(counts.get('denied_files') or 0) + int(counts.get('unverified_files') or 0)
try:
activity_record = {
'id': str(uuid.uuid4()),
'user_id': normalized_user_id,
'activity_type': 'azure_files_search_access',
'timestamp': now,
'created_at': now,
'action': 'results_withheld',
'description': (
f"Azure Files Search withheld {withheld} of {int(counts.get('files_evaluated') or 0)} files "
f"({int(counts.get('denied_files') or 0)} denied, {int(counts.get('unverified_files') or 0)} unverified)"
),
'conversation_id': (review or {}).get('conversation_id') or None,
'agent': (review or {}).get('agent') or None,
'action_context': {
'action_id': (review or {}).get('action_id'),
'action_name': (review or {}).get('action_name'),
'display_name': (review or {}).get('display_name'),
'search_service': (review or {}).get('search_service'),
'index_name': (review or {}).get('index_name'),
},
'additional_context': {
'status': (review or {}).get('status'),
'permission_mode': (review or {}).get('permission_mode'),
'share_access_check': (review or {}).get('share_access_check'),
'counts': counts,
'reasons': (review or {}).get('reasons') or {},
'withheld_files': (review or {}).get('withheld_files') or [],
'withheld_files_truncated': bool((review or {}).get('withheld_files_truncated')),
'duration_ms': (review or {}).get('duration_ms'),
},
}
cosmos_activity_logs_container.create_item(body=activity_record)
debug_print("[AZURE_FILES_SEARCH] Access review logged.")
except Exception as e:
log_event(
message="[AZURE_FILES_SEARCH] Error logging access review.",
extra={
'user_id': normalized_user_id,
'action_id': (review or {}).get('action_id'),
'exception_type': type(e).__name__,
},
level=logging.ERROR
)


def log_governance_change(
admin_user_id: str,
admin_email: str,
Expand Down
37 changes: 37 additions & 0 deletions application/single_app/functions_azure_endpoint_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
"api.loganalytics.us",
"api.loganalytics.azure.cn",
)
# Azure AI Search service suffixes for the public, US Government, and China clouds.
AZURE_SEARCH_ENDPOINT_SUFFIXES = (
"search.windows.net",
"search.azure.us",
"search.azure.cn",
)
# Mirrors azure.identity.AzureAuthorityHosts so a caller cannot select a token authority.
AZURE_ENTRA_AUTHORITY_HOSTS = (
"login.microsoftonline.com",
Expand Down Expand Up @@ -106,6 +112,14 @@
"The application identity can be used only with an HTTPS Azure AI endpoint in this "
"cloud, such as https://resource.openai.azure.com"
)
AZURE_SEARCH_ENDPOINT_ERROR = (
"Azure AI Search actions require an HTTPS Azure AI Search service endpoint such as "
"https://service.search.windows.net"
)
AZURE_FILE_ENDPOINT_ERROR = (
"Azure Files requires an HTTPS Azure Files service endpoint such as "
"https://account.file.core.windows.net"
)


def _normalize_endpoint_text(value: Any) -> str:
Expand Down Expand Up @@ -243,6 +257,29 @@ def validate_azure_queue_endpoint(value: Any) -> str:
return _validate_storage_endpoint(value, AZURE_QUEUE_SERVICE_LABEL, AZURE_QUEUE_ENDPOINT_ERROR)


def validate_azure_file_endpoint(value: Any) -> str:
"""Return a canonical Azure Files service origin, or raise ValueError."""
return _validate_storage_endpoint(value, AZURE_FILE_SERVICE_LABEL, AZURE_FILE_ENDPOINT_ERROR)


def validate_azure_search_endpoint(value: Any) -> str:
"""Return a canonical Azure AI Search service origin, or raise ValueError."""
_, hostname = parse_azure_https_endpoint(
value,
AZURE_SEARCH_ENDPOINT_ERROR,
allow_default_port=True,
)
service_name, endpoint_suffix = _match_endpoint_suffix(
hostname,
"",
AZURE_SEARCH_ENDPOINT_SUFFIXES,
AZURE_SEARCH_ENDPOINT_ERROR,
)
if not DNS_LABEL_PATTERN.match(service_name) or not 2 <= len(service_name) <= 60:
raise ValueError(AZURE_SEARCH_ENDPOINT_ERROR)
return f"https://{service_name}.{endpoint_suffix}"


def validate_azure_cosmos_endpoint(value: Any) -> str:
"""Return a canonical Azure Cosmos DB origin, or raise ValueError."""
_, hostname = parse_azure_https_endpoint(
Expand Down
Loading
Loading