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
18 changes: 12 additions & 6 deletions extensions/business/cybersec/red_mesh/mixins/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,13 @@ def _stamp_worker_source(self, local_job_status, worker_id, node_addr):
self._stamp_finding_list(local_job_status.get("findings"),
worker_id, node_addr)

def _get_aggregated_report(self, local_jobs, worker_cls=None):
def _get_aggregated_report(self, local_jobs, worker_cls=None, log_details=True):
"""
Aggregate results from multiple local workers.

``log_details=False`` is used at request trust boundaries where aggregate
content must not be copied into runtime logs on failure.

Parameters
----------
local_jobs : dict
Expand Down Expand Up @@ -429,11 +432,14 @@ def _get_aggregated_report(self, local_jobs, worker_cls=None):
self.P(f"Report aggregation done.")
# endif we have local jobs
except Exception as exc:
self.P("Error during report aggregation: {}:\n{}\n{}\ntype_or_func={}, field={}".format(
exc, self.trace_info(),
self.json_dumps(dct_aggregated_report, indent=2),
type_or_func, field
))
if log_details:
self.P("Error during report aggregation: {}:\n{}\n{}\ntype_or_func={}, field={}".format(
exc, self.trace_info(),
self.json_dumps(dct_aggregated_report, indent=2),
type_or_func, field
))
else:
self.P("Manual report aggregation failed", color='y')
# Phase 0 dedup pass: collapse findings duplicated across workers
# because each worker stamps its own _source_worker_id /
# _source_node_addr before merge. The JSON-key fallback in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ def run(self):
raw_evidence_cases.append(case_evidence)
metrics["completed_cases"] += 1
results.append(case_result)
overall_status = "completed" if metrics["evaluated_cases"] == metrics["total_cases"] else "incomplete"
overall_status = "complete" if metrics["evaluated_cases"] == metrics["total_cases"] else "incomplete"
if not results:
overall_status = "failed"
summary = {
Expand Down
78 changes: 50 additions & 28 deletions extensions/business/cybersec/red_mesh/model_testing/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from __future__ import annotations

import hmac
import ipaddress
import os
import socket
from urllib.parse import urlsplit


MODEL_PROVIDER_CREDENTIAL_UNAVAILABLE = "credential_unavailable"
_MODEL_PROVIDER_REF_PREFIX = "model_provider/"
BACKEND_TOKEN_ENV = "REDMESH_BACKEND_TOKEN"
MIN_BACKEND_TOKEN_BYTES = 32
_PROVIDER_ALLOWED_KEYS = {
"adapter",
"provider_label",
Expand Down Expand Up @@ -143,11 +146,51 @@ def validate_provider_url(base_url, *, resolver=None):

def _credential_error():
return _validation_error(
MODEL_PROVIDER_CREDENTIAL_UNAVAILABLE,
"Provider requires an API key payload.",
error_class=MODEL_PROVIDER_CREDENTIAL_UNAVAILABLE,
)


def _backend_auth_error(*, status_code, error, error_class, message):
return {
"status": "error",
"status_code": status_code,
"error": error,
"error_class": error_class,
"message": message,
}


def validate_backend_token(token):
"""Validate the Navigator-to-edge bearer token without exposing token material."""
expected = os.environ.get(BACKEND_TOKEN_ENV, "")
expected_bytes = expected.encode("utf-8")
if len(expected_bytes) < MIN_BACKEND_TOKEN_BYTES:
return _backend_auth_error(
status_code=401,
error="unauthorized",
error_class="backend_auth_unavailable",
message="Backend authentication is not configured.",
)

presented = token if isinstance(token, str) else ""
if not presented:
return _backend_auth_error(
status_code=401,
error="unauthorized",
error_class="backend_auth_required",
message="Backend authentication is required.",
)
if not hmac.compare_digest(presented.encode("utf-8"), expected_bytes):
return _backend_auth_error(
status_code=403,
error="forbidden",
error_class="backend_auth_invalid",
message="Backend authentication failed.",
)
return None


def validate_provider_config_shape(provider, *, role):
"""Reject inline credential-bearing or unknown provider config fields."""
if not isinstance(provider, dict):
Expand Down Expand Up @@ -182,32 +225,11 @@ def validate_model_provider_credentials(
if isinstance(secret_payload, dict):
api_key = str(secret_payload.get("api_key") or "")
has_secret = bool(api_key)
if credential_ref and has_secret:
return None, _validation_error(
f"{role} may not supply both credential_ref and secret payload",
error_class="duplicate_credential_source",
)
# Credential references are an accepted historical shape but there is no
# worker-local resolver yet. Reject them before launch/preflight persistence
# with one sanitized error that never includes the supplied identifier.
if credential_ref:
return None, _credential_error()
if has_secret:
return {"source": "secret_payload", "credential_ref_present": False}, None
if not credential_ref:
return None, _validation_error(
f"{role} requires credential_ref or secret payload",
error_class=MODEL_PROVIDER_CREDENTIAL_UNAVAILABLE,
)
if not credential_ref.startswith(_MODEL_PROVIDER_REF_PREFIX):
return None, _credential_error()
ref_body = credential_ref[len(_MODEL_PROVIDER_REF_PREFIX):]
operator_prefix = f"operator/{created_by_id}/"
if ref_body.startswith(operator_prefix):
credential_id = ref_body[len(operator_prefix):]
if not credential_id or "/" in credential_id:
return None, _credential_error()
return {"source": "credential_ref", "credential_ref_present": True}, None
if ref_body.startswith("deploy/default_evaluator/"):
if role != "evaluator_model" or not use_default_evaluator_model:
return None, _credential_error()
credential_id = ref_body[len("deploy/default_evaluator/"):]
if not credential_id or "/" in credential_id:
return None, _credential_error()
return {"source": "credential_ref", "credential_ref_present": True}, None
return None, _credential_error()
26 changes: 26 additions & 0 deletions extensions/business/cybersec/red_mesh/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@
FindingTriageState,
VALID_TRIAGE_STATUSES,
)
from extensions.business.cybersec.red_mesh.models.rulebook import (
RULEBOOK_ASSESSMENT_SCHEMA,
RULEBOOK_ASSESSMENT_SCHEMA_VERSION,
RULEBOOK_SUBMISSION_CONTRACT_VERSION,
RulebookPendingSubmission,
RulebookReviewAuditEntry,
RulebookReviewState,
RulebookSubmissionReference,
RulebookSubmissionRegistry,
VALID_RULEBOOK_ANSWER_VALUES,
VALID_RULEBOOK_CHECK_STATUSES,
VALID_RULEBOOK_REVIEW_STATES,
VALID_RULEBOOK_SUBMISSION_STATES,
)
from extensions.business.cybersec.red_mesh.models.engagement import (
ASSET_EXPOSURES,
AuthorizationRef,
Expand Down Expand Up @@ -109,6 +123,18 @@
"FindingTriageState",
"FindingTriageAuditEntry",
"VALID_TRIAGE_STATUSES",
"RULEBOOK_ASSESSMENT_SCHEMA",
"RULEBOOK_ASSESSMENT_SCHEMA_VERSION",
"RULEBOOK_SUBMISSION_CONTRACT_VERSION",
"RulebookReviewState",
"RulebookReviewAuditEntry",
"RulebookSubmissionReference",
"RulebookPendingSubmission",
"RulebookSubmissionRegistry",
"VALID_RULEBOOK_ANSWER_VALUES",
"VALID_RULEBOOK_CHECK_STATUSES",
"VALID_RULEBOOK_REVIEW_STATES",
"VALID_RULEBOOK_SUBMISSION_STATES",
# engagement
"Contact",
"EngagementContext",
Expand Down
4 changes: 4 additions & 0 deletions extensions/business/cybersec/red_mesh/models/cstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class CStoreJobRunning:
stix_export: dict = None
opencti_export: dict = None
taxii_export: dict = None
rulebook_assessments: dict = None
graybox_assignment_summary: dict = None
blockchain_attestation_enabled: bool = False
start_attestation_required: bool = False
Expand Down Expand Up @@ -195,6 +196,7 @@ def from_dict(cls, d: dict) -> CStoreJobRunning:
stix_export=d.get("stix_export"),
opencti_export=d.get("opencti_export"),
taxii_export=d.get("taxii_export"),
rulebook_assessments=d.get("rulebook_assessments"),
graybox_assignment_summary=d.get("graybox_assignment_summary"),
blockchain_attestation_enabled=d.get("blockchain_attestation_enabled", False),
start_attestation_required=d.get("start_attestation_required", False),
Expand Down Expand Up @@ -243,6 +245,7 @@ class CStoreJobFinalized:
stix_export: dict = None
opencti_export: dict = None
taxii_export: dict = None
rulebook_assessments: dict = None
graybox_assignment_summary: dict = None
blockchain_attestation_enabled: bool = False
start_attestation_required: bool = False
Expand Down Expand Up @@ -290,6 +293,7 @@ def from_dict(cls, d: dict) -> CStoreJobFinalized:
stix_export=d.get("stix_export"),
opencti_export=d.get("opencti_export"),
taxii_export=d.get("taxii_export"),
rulebook_assessments=d.get("rulebook_assessments"),
graybox_assignment_summary=d.get("graybox_assignment_summary"),
blockchain_attestation_enabled=d.get("blockchain_attestation_enabled", False),
start_attestation_required=d.get("start_attestation_required", False),
Expand Down
Loading
Loading