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
31 changes: 21 additions & 10 deletions app/backend/app/pipeline/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import re
from typing import Optional

from ..services import coverage

from .models import (
ControlPolicyMapping,
ControlExtractionResult,
Expand Down Expand Up @@ -111,19 +113,26 @@ def validate_mappings(
automatable_count = 0
manual_count = 0
confidence_sum = 0.0
confidence_eligible_count = 0

for mapping in mappings:
control_id = mapping.control_id

# Check confidence score
confidence_sum += mapping.confidence_score
if mapping.confidence_score < min_confidence:
issues.append(ValidationIssue(
severity="warning",
control_id=control_id,
message=f"Low confidence mapping ({mapping.confidence_score:.2f} < {min_confidence})",
suggestion="Review this mapping manually before deployment",
))
# Check confidence score. Scoped to controls where Azure Policy was
# actually attempted: C_Process/D_MicrosoftAttestation mappings carry
# a fixed 0.0 confidence_score placeholder (no match was ever tried),
# so including them here would both drag the average down misleadingly
# and raise a spurious "low confidence" warning on every one of them.
if coverage.confidence_eligible(getattr(mapping, "coverage_category", None)):
confidence_sum += mapping.confidence_score
confidence_eligible_count += 1
if mapping.confidence_score < min_confidence:
issues.append(ValidationIssue(
severity="warning",
control_id=control_id,
message=f"Low confidence mapping ({mapping.confidence_score:.2f} < {min_confidence})",
suggestion="Review this mapping manually before deployment",
))

# Check Azure Policy IDs
if mapping.is_automatable:
Expand Down Expand Up @@ -179,7 +188,9 @@ def validate_mappings(
suggestion="Add guidance on what manual steps or evidence are needed",
))

avg_confidence = confidence_sum / len(mappings) if mappings else 0.0
avg_confidence = (
confidence_sum / confidence_eligible_count if confidence_eligible_count else 0.0
)

error_count = sum(1 for i in issues if i.severity == "error")
is_valid = error_count == 0
Expand Down
22 changes: 20 additions & 2 deletions app/backend/app/services/ai_mapping_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ async def map_control(

self._apply_procedural_sovereignty(mapping, external_control)

# A process/attestation control that carries no real sovereignty
# objective should not show the model's default L1/sovereign_root
# placeholder as if it were a genuine sovereignty verdict - see
# clear_moot_sovereignty for why. Must run after apply_coverage
# (needs coverage_category) and after the procedural sovereignty
# step above (which may have attached a real objective to keep).
coverage.clear_moot_sovereignty(mapping)

# policy_category is a resolvable fact about the real catalog
# entries selected, not a judgement call, so it is always computed
# here rather than asked of the model - same rationale as
Expand Down Expand Up @@ -415,9 +423,19 @@ async def map_one(control: ExternalControl) -> tuple[str, Optional[ControlMappin
mappings.append(mapping)

mapped_count = len(mappings)
# Scope the average to controls where confidence is a real signal.
# C_Process/D_MicrosoftAttestation mappings never attempt an Azure
# Policy match, so their confidence_score is a fixed 0.0 placeholder -
# averaging it in with A/B controls' genuine scores drags a batch that
# matched its enforceable controls well down to a misleadingly low
# headline number. See coverage.confidence_eligible.
confidence_scoped = [
m for m in mappings
if coverage.confidence_eligible(getattr(m, "coverage_category", None))
]
avg_confidence = (
sum(m.confidence_score for m in mappings) / mapped_count
if mapped_count > 0 else 0.0
sum(m.confidence_score for m in confidence_scoped) / len(confidence_scoped)
if confidence_scoped else 0.0
)

summary = self._generate_summary(total_controls, mapped_count, avg_confidence)
Expand Down
61 changes: 61 additions & 0 deletions app/backend/app/services/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@
# so stripping its policies deletes real enforcement the customer is entitled to.
POLICY_BEARING_CATEGORIES = frozenset({COVERAGE_A, COVERAGE_B})

# Categories for which confidence_score is never a graded match-quality signal.
# C_Process and D_MicrosoftAttestation controls never attempt an Azure Policy
# match - confidence_score is forced to 0.0 as a placeholder (see
# ai_mapping_service.py's ControlMapping construction), not a real assessment
# of how well anything matched. Averaging that fixed 0.0 in with A/B controls'
# genuine confidence judgements drags a legitimate "every enforceable control
# matched well" result down to a misleading low headline number - exactly the
# defect this constant exists to prevent. ``None`` (legacy/unclassified
# mappings) is deliberately NOT included here, for the same backward-
# compatibility reason coverage_summary buckets it as "unclassified" rather
# than excluding it.
CONFIDENCE_EXCLUDED_CATEGORIES = frozenset({COVERAGE_C, COVERAGE_D})


def confidence_eligible(category: Optional[str]) -> bool:
"""True when a mapping's confidence_score is a meaningful match-quality signal.

Use this to scope any "average confidence" / "high confidence" statistic
to the controls where confidence was actually judged - A_AzurePolicy and
B_AzureConfig (plus legacy ``None`` mappings, for backward compatibility).
C_Process and D_MicrosoftAttestation controls are excluded: Azure was
never attempted for them, so their fixed 0.0 confidence_score is a
placeholder, not a low score.
"""
return category not in CONFIDENCE_EXCLUDED_CATEGORIES


# The analyst's display names, from the source workbook's Legend sheet. The
# A_/B_/C_/D_ codes are internal identifiers; these are what a reader sees.
COVERAGE_DISPLAY_NAMES = {
Expand Down Expand Up @@ -374,6 +401,40 @@ def apply_coverage(
return mapping


def clear_moot_sovereignty(mapping) -> None:
"""Clear a C/D control's sovereignty card when it carries no real objective.

The mapping prompt asks the model to assign a sovereignty dimension to
EVERY control, defaulting to L1/sovereign_root with an empty objectives
list when none applies. For a process or Microsoft-attestation control
that will never carry an Azure Policy or SLZ policy, that default renders
as a real assignment ("Level: L1 - Global", "Target Archetype:
sovereign_root") when the model's own sovereignty reasoning says the
opposite - no sovereignty requirement applies here at all. Shown next to
a mapping that constructs no initiative entry, it reads as a second,
contradictory verdict rather than a placeholder.

The one legitimate exception is a real procedural sovereignty objective
(e.g. SO-2 Customer Lockbox), which has no Azure Policy but genuine
sovereignty relevance - callers apply that (see
``AIMappingService._apply_procedural_sovereignty``) before this runs, so
it is preserved by checking for a non-empty ``sovereignty_objectives``
rather than category alone.

Must run after ``apply_coverage`` (needs the resolved ``coverage_category``)
and after any procedural sovereignty enrichment.
"""
category = getattr(mapping, "coverage_category", None)
if category not in (COVERAGE_C, COVERAGE_D):
return
sovereignty = getattr(mapping, "sovereignty", None)
if sovereignty is None:
return
if getattr(sovereignty, "sovereignty_objectives", None):
return # a real procedural objective is attached - keep it
mapping.sovereignty = None


def apply_provenance(mapping, catalog=None):
"""Record what verified this mapping and when.

Expand Down
38 changes: 30 additions & 8 deletions app/frontend/pages/2_AI_Mapping.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""
AI Mapping Page - Map controls to MCSB using AI.
AI Mapping Page - Map controls to Azure Policy using AI.
"""

import streamlit as st
from utils.api_client import get_api_client
from utils.theme import inject_azure_theme, render_sidebar, render_footer
from utils.components import render_page_header
from utils.state_init import init_session_state, restore_workflow_state
from utils.coverage import confidence_eligible
from utils.task_manager import (
cancel_task,
register_task,
Expand Down Expand Up @@ -227,6 +228,12 @@ def _session_mapping_from_result(mapping: dict, controls: list) -> dict:
"enforcement_plane": mapping.get("enforcement_plane"),
"policy_effects": mapping.get("policy_effects", []),
"policy_type": mapping.get("policy_type"),
# For D_MicrosoftAttestation: the resolved citation an auditor could
# actually retrieve, and whether the claim is grounded at all. Without
# these the Manual Register can only assert "Microsoft attested" with
# nothing to back it, which is indistinguishable from an invented pass.
"attestation": mapping.get("attestation"),
"attestation_gap": mapping.get("attestation_gap", False),
}


Expand Down Expand Up @@ -466,19 +473,33 @@ def _render_active_mapping_job(
if completed_notice := st.session_state.pop("mapping_completed_notice", None):
st.success(f"✅ {completed_notice}")
mappings = st.session_state.mappings
# Scope confidence stats to controls where Azure Policy mapping was
# actually attempted (A/B, plus legacy unclassified). C_Process and
# D_MicrosoftAttestation controls carry a fixed 0.0 confidence_score
# placeholder - averaging it in misrepresents how well the real
# mapping work went. See utils/coverage.py.
scored_mappings = [m for m in mappings if confidence_eligible(m)]
col_sum1, col_sum2, col_sum3 = st.columns(3)
with col_sum1:
avg_confidence = (
sum(mapping.get("confidence_score", 0) for mapping in mappings)
/ len(mappings)
if mappings
sum(mapping.get("confidence_score", 0) for mapping in scored_mappings)
/ len(scored_mappings)
if scored_mappings
else 0
)
st.metric("Average Confidence", f"{avg_confidence:.0%}")
st.metric(
"Average Confidence",
f"{avg_confidence:.0%}",
help=(
f"Across {len(scored_mappings)} Azure-mappable control(s); "
"excludes process/Microsoft-attested controls, which never "
"attempt an Azure Policy match."
),
)
with col_sum2:
high_confidence = sum(
1
for mapping in mappings
for mapping in scored_mappings
if mapping.get("confidence_score", 0) >= 0.8
)
st.metric("High Confidence (≥80%)", high_confidence)
Expand Down Expand Up @@ -617,8 +638,9 @@ def _render_active_mapping_job(
if st.session_state.mappings:
st.success(f"✅ {len(st.session_state.mappings)} mappings created")

# Statistics
avg_conf = sum(m.get('confidence_score', 0) for m in st.session_state.mappings) / len(st.session_state.mappings)
# Statistics — scoped to Azure-mappable controls (see utils/coverage.py)
scored = [m for m in st.session_state.mappings if confidence_eligible(m)]
avg_conf = sum(m.get('confidence_score', 0) for m in scored) / len(scored) if scored else 0
st.metric("Avg Confidence", f"{avg_conf:.0%}")
else:
st.info("No mappings yet")
Expand Down
Loading
Loading