Skip to content

FEAT : Propagated Scope #9

Description

@junkoku38

Feature: Sandbox du scope propagé sur footholds

Contexte

AttackSim propage le scanner sur des footholds via --propagate (Docker, kubelet, SSH, ADB, cloud). L'instance propagée hérite du ScopeGuard du parent — mais ScopeGuard n'expose pas de méthode de sérialisation (.specs), et l'héritage de scope échoue silencieusement. L'instance propagée se retrouve avec un guard videauthorize() retourne True pour tout hôte à intrusive+ → le scanner déployé dans un container d'un réseau interne 10.0.5.0/24 peut scanner toute la /24 voire au-delà sans aucune restriction.

La promesse "every peer is scope-checked" est vraie pour le scanner parent, fausse pour l'instance propagée.

Description

  1. Exposer une méthode de sérialisation du ScopeGuard pour transmettre le scope à l'instance propagée.
  2. Toujours injecter --scope <foothold_subnet> comme borne minimale dans la propagation.
  3. Refuser de scanner hors du sous-réseau du foothold sauf --scope explicite.

Spécifications fonctionnelles

Comportement attendu

  1. Quand --propagate déploie le scanner sur un foothold à l'adresse 10.0.5.42 :

    • Le /24 dérivé est 10.0.5.0/24
    • L'instance propagée reçoit --scope 10.0.5.0/24 en plus du scope parent
    • L'instance propagée ne peut scanner que dans 10.0.5.0/24 (et le scope parent si fourni)
  2. Si le scope parent contient 10.0.0.0/16 :

    • L'instance propagée reçoit l'intersection (10.0.5.0/24 étant plus restrictif)
    • En pratique : --scope 10.0.5.0/24 prime
  3. Si l'opérateur fournit --scope 10.0.0.0/16 explicitement au parent :

    • L'instance propagée reçoit --scope 10.0.0.0/16 (scope opérateur respecté)
    • Mais un warning est émis : "Propagated instance can scan 10.0.0.0/16 from foothold 10.0.5.42"
  4. Si aucun scope n'est fourni :

    • L'instance propagée reçoit --scope 10.0.5.0/24 (borne minimale de sécurité)
    • Warning : "No explicit scope — propagated instance limited to /24 of foothold"

Refus de scan hors scope

Si l'instance propagée tente de scanner 10.0.99.5 (hors 10.0.5.0/24 et hors scope parent) :

[!] scope_guard: refusing to scan 10.0.99.5:80 — out of propagated scope (10.0.5.0/24)

Spécifications techniques

Fichier : sandbox/scope_guard.py

Ajouter serialize() et deserialize()

def serialize(self) -> dict:
    """Serialize the scope for propagation."""
    return {
        "hosts": list(self._hosts),
        "domains": list(self._domains),
        "networks": [str(n) for n in self._networks],
        "ports": list(self._ports),
        "intensity": self._intensity,
        "empty": self.empty,
    }

@classmethod
def deserialize(cls, data: dict) -> "ScopeGuard":
    """Reconstruct a ScopeGuard from serialized data."""
    g = cls.__new__(cls)
    g._hosts = set(data.get("hosts", []))
    g._domains = set(data.get("domains", []))
    g._networks = [ipaddress.ip_network(n) for n in data.get("networks", [])]
    g._ports = set(data.get("ports", []))
    g._intensity = data.get("intensity", "intrusive")
    g._empty = data.get("empty", True)
    return g

Ajouter foothold_subnet(ip) -> str

def foothold_subnet(ip: str, prefix: int = 24) -> str:
    """Derive the /24 (default) subnet for a foothold IP."""
    addr = ipaddress.ip_address(ip)
    net = ipaddress.ip_network(f"{addr}/{prefix}", strict=False)
    return str(net)

Fichier : main.py_build_propagation_args (ligne 1272-1349)

Changements

  1. Calculer le subnet du foothold :

    foothold_ip = foothold.get("ip")  # ex: "10.0.5.42"
    derived_subnet = scope_guard.foothold_subnet(foothold_ip, prefix=24)
  2. Sérialiser le scope parent et l'injecter dans le payload propagé :

    scope_data = guard.serialize()
    # Ajouter le subnet du foothold comme borne minimale
    if not scope_data["networks"]:
        scope_data["networks"] = [derived_subnet]
    else:
        # Intersection : ne garder que les réseaux qui contiennent le foothold
        scope_data["networks"] = [
            n for n in scope_data["networks"]
            if ipaddress.ip_network(n).supernet_of(ipaddress.ip_network(derived_subnet))
        ] or [derived_subnet]
  3. Ajouter --scope aux args du scanner propagé :

    args_list.extend(["--scope", ",".join(scope_data["networks"])])
  4. Si le scope parent est vide (pas de --scope opérateur) :

    if guard.empty:
        print(f"[!] No explicit scope — propagated instance limited to {derived_subnet}", file=sys.stderr)
        args_list.extend(["--scope", derived_subnet])

Fichier : sandbox/propagate_pack.py

Ajouter le scope sérialisé dans le manifest JSON sidecar :

manifest["scope"] = scope_data

Fichier : config.pyimport_from_propagation (ligne 986-989)

Ajouter la validation du scope propagé :

def import_from_propagation(data: dict) -> None:
    # Existing overlay import (with allow-list, cf. FEATURE_redaction)
    for k, v in data.get("config", {}).items():
        if k in _PROPAGATABLE_KEYS:
            _RUNTIME_OVERLAY[k] = v
    # Import scope
    if "scope" in data:
        _PROPAGATED_SCOPE = ScopeGuard.deserialize(data["scope"])

Tests

tests/test_propagated_scope.py (nouveau) :

  • ScopeGuard.serialize()deserialize() → round-trip identique
  • Propagation avec scope parent 10.0.0.0/16 + foothold 10.0.5.42 → propagated scope = 10.0.5.0/24 (intersection)
  • Propagation sans scope parent + foothold 10.0.5.42 → propagated scope = 10.0.5.0/24
  • Propagation avec scope parent 192.168.0.0/24 + foothold 10.0.5.42 → propagated scope = 10.0.5.0/24 (pas d'intersection → borne minimale)
  • authorize("10.0.5.1", 80) dans scope 10.0.5.0/24 → True
  • authorize("10.0.99.5", 80) dans scope 10.0.5.0/24 → False
  • Vérifier que le warning est émis quand scope parent vide

tests/test_propagation_v2.py (existant) — vérifier que les tests passent toujours.

Acceptance criteria

  • ScopeGuard.serialize() / deserialize() round-trip
  • Propagation injecte --scope <foothold_subnet> comme borne minimale
  • Intersection du scope parent avec le subnet du foothold
  • Warning émis si aucun scope opérateur explicite
  • Instance propagée refuse de scanner hors scope
  • Tests unitaires passent
  • Tests de propagation existants non cassés

Références

  • Issue parent : REVIEW.md I1 — --propagate déploie sans appliquer le scope aux sous-réseaux découverts
  • sandbox/scope_guard.py:318-343authorize() retourne True à intrusive+ avec allow-list vide
  • main.py:1272-1349_build_propagation_args

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions