Skip to content
Draft
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: 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