AID-H-024.003: Fail-Closed Ambiguous Resolution with Typosquat Detection

Version 1.20261015 · Actionable sub-technique

Detect ambiguous matches and near-miss tool names and fail closed. Similarity checks must trigger explicit disambiguation rather than auto-selection.

Pillars: app

Lifecycle: operation

Scope boundary

Owns detection of name collisions, near misses, typosquats, and multiple eligible resolution candidates, and requires explicit disambiguation. It does not choose or authorize a candidate on similarity alone.

AID-H-024.001: AID-H-024.003 blocks ambiguous or suspicious candidate selection; AID-H-024.001 requires the resulting reference to use a canonical namespace and pinned version. Disambiguation determines the candidate, while full qualification makes that choice stable.

Implementation guidance

AID-H-024.003-G001

If multiple candidates exist, stop and require explicit selection; block near-miss names based on similarity threshold.

Use this guard at tool discovery, onboarding, registry search, or any legacy path that can return multiple tool candidates. It never chooses a runnable tool from a similarity score. The execution path still requires the exact fully qualified ID and signed allowlist decision from AID-H-024.001. Similarity only explains why a non-exact request is blocked and routed for explicit review.

Evaluate the complete candidate population
# File: tool_resolution/ambiguity_guard.py
from __future__ import annotations

import hashlib
import json
import re
from dataclasses import dataclass

from rapidfuzz import fuzz

FQID = re.compile(
    r"^[a-z0-9][a-z0-9._-]{0,62}/[a-z0-9][a-z0-9._-]{0,62}@"
    r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)"
    r"(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$"
)
SHA256 = re.compile(r"^[0-9a-f]{64}$")


@dataclass(frozen=True)
class VerifiedTyposquatPolicy:
    similarity_threshold: int
    policy_version: str
    policy_sha256: str
    signature_sha256: str


def assess_resolution(
    requested: str,
    candidate_ids: list[str],
    policy: VerifiedTyposquatPolicy,
) -> dict:
    if not isinstance(requested, str) or not 1 <= len(requested) <= 190:
        return {"status": "ERROR", "reason": "invalid_requested_identifier"}
    if not isinstance(candidate_ids, list) or not candidate_ids:
        return {"status": "INSUFFICIENT_DATA", "reason": "candidate_population_missing"}
    if any(not isinstance(item, str) or not FQID.fullmatch(item) for item in candidate_ids):
        return {"status": "ERROR", "reason": "invalid_candidate_identifier"}
    if len(candidate_ids) != len(set(candidate_ids)):
        return {"status": "ERROR", "reason": "duplicate_registry_candidate"}
    if not isinstance(policy, VerifiedTyposquatPolicy):
        return {"status": "INSUFFICIENT_DATA", "reason": "verified_policy_missing"}
    if type(policy.similarity_threshold) is not int or not 1 <= policy.similarity_threshold <= 100:
        return {"status": "ERROR", "reason": "invalid_policy_threshold"}
    if not SHA256.fullmatch(policy.policy_sha256) or not SHA256.fullmatch(policy.signature_sha256):
        return {"status": "ERROR", "reason": "invalid_policy_receipt"}

    population = sorted(candidate_ids)
    population_sha256 = hashlib.sha256(
        json.dumps(population, separators=(",", ":")).encode("utf-8")
    ).hexdigest()
    exact = [item for item in population if item == requested]
    if len(exact) == 1:
        return {
            "schema_version": "aidefend.h024003-resolution.v1",
            "status": "PASS",
            "requested": requested,
            "selected_tool_fqid": exact[0],
            "candidate_population_sha256": population_sha256,
            "candidate_count": len(population),
            "policy_version": policy.policy_version,
            "policy_sha256": policy.policy_sha256,
        }

    requested_key = requested.casefold()
    scores = sorted(
        (
            {"tool_fqid": item, "score": fuzz.ratio(requested_key, item.casefold())}
            for item in population
        ),
        key=lambda row: (-row["score"], row["tool_fqid"]),
    )
    near_matches = [
        row for row in scores if row["score"] >= policy.similarity_threshold
    ]
    return {
        "schema_version": "aidefend.h024003-resolution.v1",
        "status": "FAIL",
        "reason": "near_match_requires_explicit_fqid" if near_matches else "exact_match_required",
        "requested": requested,
        "near_matches": near_matches,
        "candidate_population_sha256": population_sha256,
        "candidate_count": len(population),
        "policy_version": policy.policy_version,
        "policy_sha256": policy.policy_sha256,
    }
Calibration and enforcement

Derive the threshold from a versioned labeled set containing approved names, common typing errors, homoglyph-normalized variants, affix squats, and benign similarly named tools. Calibration may run in log-only mode in a non-executing test environment; production resolution remains fail-closed throughout. A read-only verifier replays the complete recorded candidate population and policy release, reproduces the result, and confirms that only a single exact PASS FQID was handed to AID-H-024.001.

Action: Delete best-match and first-result selection. For every registry response, retain the complete candidate-set digest and signed threshold receipt; return a runnable ID only for one exact FQID match, and route every ambiguity or near match to explicit operator selection and subsequent allowlist approval.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-024.003: Fail-Closed Ambiguous Resolution with Typosquat Detection | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version