feat: project scaffold, unified Pydantic schema, scope gating

This commit is contained in:
2026-06-29 17:35:50 -04:00
commit 1f531e4948
9 changed files with 1239 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
.git
.venv
out
**/__pycache__
*.pyc
*.egg-info
.pytest_cache
.ruff_cache
scope.yaml
+18
View File
@@ -0,0 +1,18 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.eggs/
build/
dist/
.venv/
.pytest_cache/
.ruff_cache/
# Runtime output (operator data — never committed)
/out/
report.json
report.md
# Local scope file may contain real target lists — keep the example only.
/scope.yaml
+48
View File
@@ -0,0 +1,48 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "recon-triage"
version = "0.1.0"
description = "Defensive bug-bounty recon & triage pipeline (authorized testing only)."
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "recon-triage" }]
dependencies = [
"pydantic>=2.6,<3",
"typer>=0.12,<1",
"pyyaml>=6.0",
"xmltodict>=0.13",
"httpx>=0.27",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"ruff>=0.4",
]
[project.scripts]
recon-triage = "recon_triage.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "W", "UP", "B"]
ignore = ["E501", "UP042"] # UP042: `str, Enum` is intentional for broad compatibility
[tool.ruff.lint.per-file-ignores]
# typer.Option() in argument defaults is the canonical Typer idiom.
"src/recon_triage/cli.py" = ["B008"]
@@ -0,0 +1,709 @@
{
"$defs": {
"ExploitDBMatch": {
"additionalProperties": false,
"description": "An Exploit-DB candidate returned verbatim by ``searchsploit``.\n\nThese are ALWAYS unverified references. We never fabricate or infer EDB-IDs or\npaths; only what searchsploit actually returned is emitted.",
"properties": {
"edb_id": {
"description": "Exploit-DB id, e.g. '50383'.",
"title": "Edb Id",
"type": "string"
},
"title": {
"title": "Title",
"type": "string"
},
"path": {
"description": "Local path within the Exploit-DB checkout.",
"title": "Path",
"type": "string"
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "exploit-db.com URL.",
"title": "Url"
},
"query": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The searchsploit query that produced this hit.",
"title": "Query"
},
"source": {
"default": "exploit-db",
"title": "Source",
"type": "string"
},
"verified": {
"default": false,
"description": "Always false: candidate reference only.",
"title": "Verified",
"type": "boolean"
}
},
"required": [
"edb_id",
"title",
"path"
],
"title": "ExploitDBMatch",
"type": "object"
},
"Host": {
"additionalProperties": false,
"properties": {
"hostname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Hostname"
},
"ips": {
"items": {
"type": "string"
},
"title": "Ips",
"type": "array"
},
"cnames": {
"items": {
"type": "string"
},
"title": "Cnames",
"type": "array"
},
"ports": {
"items": {
"$ref": "#/$defs/Port"
},
"title": "Ports",
"type": "array"
}
},
"title": "Host",
"type": "object"
},
"HttpInfo": {
"additionalProperties": false,
"properties": {
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Url"
},
"status": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Status"
},
"title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Title"
},
"webserver": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Webserver"
},
"technologies": {
"items": {
"type": "string"
},
"title": "Technologies",
"type": "array"
},
"tls": {
"anyOf": [
{
"$ref": "#/$defs/TlsInfo"
},
{
"type": "null"
}
],
"default": null
}
},
"title": "HttpInfo",
"type": "object"
},
"NucleiFinding": {
"additionalProperties": false,
"properties": {
"template_id": {
"title": "Template Id",
"type": "string"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Name"
},
"severity": {
"$ref": "#/$defs/Severity",
"default": "unknown"
},
"matched_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Matched At"
},
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Host"
},
"reference": {
"items": {
"type": "string"
},
"title": "Reference",
"type": "array"
}
},
"required": [
"template_id"
],
"title": "NucleiFinding",
"type": "object"
},
"Port": {
"additionalProperties": false,
"properties": {
"number": {
"maximum": 65535,
"minimum": 0,
"title": "Number",
"type": "integer"
},
"protocol": {
"default": "tcp",
"title": "Protocol",
"type": "string"
},
"state": {
"default": "open",
"title": "State",
"type": "string"
},
"service": {
"anyOf": [
{
"$ref": "#/$defs/Service"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"number"
],
"title": "Port",
"type": "object"
},
"ScopeUsed": {
"additionalProperties": false,
"properties": {
"in_scope_domains": {
"items": {
"type": "string"
},
"title": "In Scope Domains",
"type": "array"
},
"in_scope_cidrs": {
"items": {
"type": "string"
},
"title": "In Scope Cidrs",
"type": "array"
},
"out_of_scope": {
"items": {
"type": "string"
},
"title": "Out Of Scope",
"type": "array"
}
},
"title": "ScopeUsed",
"type": "object"
},
"Service": {
"additionalProperties": false,
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Service name, e.g. 'http'.",
"title": "Name"
},
"product": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Product, e.g. 'Apache httpd'.",
"title": "Product"
},
"version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Version string, e.g. '2.4.49'.",
"title": "Version"
},
"cpe": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Cpe"
},
"http": {
"anyOf": [
{
"$ref": "#/$defs/HttpInfo"
},
{
"type": "null"
}
],
"default": null
},
"exploitdb_candidates": {
"items": {
"$ref": "#/$defs/ExploitDBMatch"
},
"title": "Exploitdb Candidates",
"type": "array"
}
},
"title": "Service",
"type": "object"
},
"Severity": {
"enum": [
"info",
"low",
"medium",
"high",
"critical",
"unknown"
],
"title": "Severity",
"type": "string"
},
"StageResult": {
"additionalProperties": false,
"properties": {
"name": {
"title": "Name",
"type": "string"
},
"status": {
"$ref": "#/$defs/StageStatus",
"default": "skipped"
},
"detail": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Detail"
},
"started_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Started At"
},
"finished_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Finished At"
}
},
"required": [
"name"
],
"title": "StageResult",
"type": "object"
},
"StageStatus": {
"description": "Per-tool stage outcome. The pipeline fails soft: a tool that crashes, times\nout, or returns nothing is recorded here and the run continues.",
"enum": [
"ok",
"empty",
"failed",
"skipped"
],
"title": "StageStatus",
"type": "string"
},
"TlsInfo": {
"additionalProperties": false,
"properties": {
"subject_cn": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Subject Cn"
},
"issuer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Issuer"
},
"not_after": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Not After"
}
},
"title": "TlsInfo",
"type": "object"
},
"ToolVersion": {
"additionalProperties": false,
"properties": {
"name": {
"title": "Name",
"type": "string"
},
"version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Version"
}
},
"required": [
"name"
],
"title": "ToolVersion",
"type": "object"
},
"TriageItem": {
"additionalProperties": false,
"description": "One prioritized finding from the LLM stage. All references must point at ids\npresent in the ReconReport \u2014 see triage.llm for the acceptance gate.",
"properties": {
"summary": {
"title": "Summary",
"type": "string"
},
"rationale": {
"title": "Rationale",
"type": "string"
},
"severity": {
"$ref": "#/$defs/Severity",
"default": "unknown"
},
"evidence_refs": {
"description": "ids/keys present in the ReconReport (host:port, edb_id, template_id).",
"items": {
"type": "string"
},
"title": "Evidence Refs",
"type": "array"
},
"suggested_next_step": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Must reference a real edb_id / nuclei template / host:port from input.",
"title": "Suggested Next Step"
},
"confidence": {
"default": 0.0,
"maximum": 1.0,
"minimum": 0.0,
"title": "Confidence",
"type": "number"
}
},
"required": [
"summary",
"rationale"
],
"title": "TriageItem",
"type": "object"
},
"TriageReport": {
"additionalProperties": false,
"properties": {
"prioritized_findings": {
"items": {
"$ref": "#/$defs/TriageItem"
},
"title": "Prioritized Findings",
"type": "array"
},
"model": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Model"
},
"generated_by": {
"default": "deterministic",
"description": "'llm' or 'deterministic'.",
"title": "Generated By",
"type": "string"
}
},
"title": "TriageReport",
"type": "object"
}
},
"additionalProperties": false,
"description": "Top-level run artifact. Schema-valid instances are written to report.json.",
"properties": {
"run_id": {
"title": "Run Id",
"type": "string"
},
"target": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Target"
},
"started_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Started At"
},
"finished_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Finished At"
},
"tool_versions": {
"items": {
"$ref": "#/$defs/ToolVersion"
},
"title": "Tool Versions",
"type": "array"
},
"scope": {
"$ref": "#/$defs/ScopeUsed"
},
"stages": {
"items": {
"$ref": "#/$defs/StageResult"
},
"title": "Stages",
"type": "array"
},
"hosts": {
"items": {
"$ref": "#/$defs/Host"
},
"title": "Hosts",
"type": "array"
},
"nuclei_findings": {
"items": {
"$ref": "#/$defs/NucleiFinding"
},
"title": "Nuclei Findings",
"type": "array"
},
"triage": {
"anyOf": [
{
"$ref": "#/$defs/TriageReport"
},
{
"type": "null"
}
],
"default": null
},
"notice": {
"default": "Authorized use only. Recon-and-triage scope: this report enumerates and references findings; it does not run or generate exploits.",
"title": "Notice",
"type": "string"
}
},
"required": [
"run_id"
],
"title": "ReconReport",
"type": "object"
}
@@ -0,0 +1,98 @@
{
"$defs": {
"Severity": {
"enum": [
"info",
"low",
"medium",
"high",
"critical",
"unknown"
],
"title": "Severity",
"type": "string"
},
"TriageItem": {
"additionalProperties": false,
"description": "One prioritized finding from the LLM stage. All references must point at ids\npresent in the ReconReport \u2014 see triage.llm for the acceptance gate.",
"properties": {
"summary": {
"title": "Summary",
"type": "string"
},
"rationale": {
"title": "Rationale",
"type": "string"
},
"severity": {
"$ref": "#/$defs/Severity",
"default": "unknown"
},
"evidence_refs": {
"description": "ids/keys present in the ReconReport (host:port, edb_id, template_id).",
"items": {
"type": "string"
},
"title": "Evidence Refs",
"type": "array"
},
"suggested_next_step": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Must reference a real edb_id / nuclei template / host:port from input.",
"title": "Suggested Next Step"
},
"confidence": {
"default": 0.0,
"maximum": 1.0,
"minimum": 0.0,
"title": "Confidence",
"type": "number"
}
},
"required": [
"summary",
"rationale"
],
"title": "TriageItem",
"type": "object"
}
},
"additionalProperties": false,
"properties": {
"prioritized_findings": {
"items": {
"$ref": "#/$defs/TriageItem"
},
"title": "Prioritized Findings",
"type": "array"
},
"model": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Model"
},
"generated_by": {
"default": "deterministic",
"description": "'llm' or 'deterministic'.",
"title": "Generated By",
"type": "string"
}
},
"title": "TriageReport",
"type": "object"
}
+17
View File
@@ -0,0 +1,17 @@
# recon-triage scope file — AUTHORIZED TARGETS ONLY.
#
# Only assets listed here are touched. Anything resolving outside these rules is
# dropped with a logged warning and never scanned. out_of_scope takes precedence.
#
# Domain rules match subdomains: "example.com" also covers "api.example.com".
in_scope_domains:
- example.com
in_scope_cidrs:
- 93.184.216.0/24
out_of_scope:
# Exclude specific hosts or sub-ranges even if they fall under an in-scope rule.
- internal-only.example.com
- 93.184.216.200/30
@@ -0,0 +1,9 @@
"""recon-triage: defensive recon & triage pipeline for authorized security testing only.
This package performs reconnaissance and triage ONLY. It never runs, generates,
downloads, or executes exploits. It enumerates assets, normalizes tool output into
a single schema, grounds findings against Exploit-DB via searchsploit, and emits a
prioritized report. A human decides what to do next.
"""
__version__ = "0.1.0"
+202
View File
@@ -0,0 +1,202 @@
"""Unified schema for recon-triage — the single source of truth for all output.
Every tool normalizer maps into this model tree, and the report/LLM stages consume
it. The JSON Schema is exported to ``schemas/`` so the contract is inspectable.
"""
from __future__ import annotations
import json
from enum import Enum
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field
class StageStatus(str, Enum):
"""Per-tool stage outcome. The pipeline fails soft: a tool that crashes, times
out, or returns nothing is recorded here and the run continues."""
OK = "ok"
EMPTY = "empty"
FAILED = "failed"
SKIPPED = "skipped"
class Severity(str, Enum):
INFO = "info"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
UNKNOWN = "unknown"
# Ordering used by the deterministic fallback ranking (higher = more urgent).
SEVERITY_RANK: dict[str, int] = {
Severity.CRITICAL.value: 5,
Severity.HIGH.value: 4,
Severity.MEDIUM.value: 3,
Severity.LOW.value: 2,
Severity.INFO.value: 1,
Severity.UNKNOWN.value: 0,
}
class _Model(BaseModel):
model_config = ConfigDict(extra="forbid", use_enum_values=True)
class ExploitDBMatch(_Model):
"""An Exploit-DB candidate returned verbatim by ``searchsploit``.
These are ALWAYS unverified references. We never fabricate or infer EDB-IDs or
paths; only what searchsploit actually returned is emitted.
"""
edb_id: str = Field(description="Exploit-DB id, e.g. '50383'.")
title: str
path: str = Field(description="Local path within the Exploit-DB checkout.")
url: str | None = Field(default=None, description="exploit-db.com URL.")
query: str | None = Field(default=None, description="The searchsploit query that produced this hit.")
source: str = Field(default="exploit-db", frozen=True)
verified: bool = Field(default=False, description="Always false: candidate reference only.")
class TlsInfo(_Model):
subject_cn: str | None = None
issuer: str | None = None
not_after: str | None = None
class HttpInfo(_Model):
url: str | None = None
status: int | None = None
title: str | None = None
webserver: str | None = None
technologies: list[str] = Field(default_factory=list)
tls: TlsInfo | None = None
class Service(_Model):
name: str | None = Field(default=None, description="Service name, e.g. 'http'.")
product: str | None = Field(default=None, description="Product, e.g. 'Apache httpd'.")
version: str | None = Field(default=None, description="Version string, e.g. '2.4.49'.")
cpe: str | None = None
http: HttpInfo | None = None
exploitdb_candidates: list[ExploitDBMatch] = Field(default_factory=list)
def query_term(self) -> str | None:
"""Build the searchsploit query: 'product version', falling back to product."""
if not self.product:
return None
if self.version:
return f"{self.product} {self.version}".strip()
return self.product.strip()
class Port(_Model):
number: int = Field(ge=0, le=65535)
protocol: str = Field(default="tcp")
state: str = Field(default="open")
service: Service | None = None
class Host(_Model):
hostname: str | None = None
ips: list[str] = Field(default_factory=list)
cnames: list[str] = Field(default_factory=list)
ports: list[Port] = Field(default_factory=list)
def key(self) -> str:
return self.hostname or (self.ips[0] if self.ips else "unknown")
class NucleiFinding(_Model):
template_id: str
name: str | None = None
severity: Severity = Severity.UNKNOWN
matched_at: str | None = None
host: str | None = None
reference: list[str] = Field(default_factory=list)
class StageResult(_Model):
name: str
status: StageStatus = StageStatus.SKIPPED
detail: str | None = None
started_at: str | None = None
finished_at: str | None = None
class ScopeUsed(_Model):
in_scope_domains: list[str] = Field(default_factory=list)
in_scope_cidrs: list[str] = Field(default_factory=list)
out_of_scope: list[str] = Field(default_factory=list)
class ToolVersion(_Model):
name: str
version: str | None = None
class TriageItem(_Model):
"""One prioritized finding from the LLM stage. All references must point at ids
present in the ReconReport — see triage.llm for the acceptance gate."""
summary: str
rationale: str
severity: Severity = Severity.UNKNOWN
evidence_refs: list[str] = Field(
default_factory=list,
description="ids/keys present in the ReconReport (host:port, edb_id, template_id).",
)
suggested_next_step: str | None = Field(
default=None,
description="Must reference a real edb_id / nuclei template / host:port from input.",
)
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
class TriageReport(_Model):
prioritized_findings: list[TriageItem] = Field(default_factory=list)
model: str | None = None
generated_by: str = Field(default="deterministic", description="'llm' or 'deterministic'.")
class ReconReport(_Model):
"""Top-level run artifact. Schema-valid instances are written to report.json."""
run_id: str
target: str | None = None
started_at: str | None = None
finished_at: str | None = None
tool_versions: list[ToolVersion] = Field(default_factory=list)
scope: ScopeUsed = Field(default_factory=ScopeUsed)
stages: list[StageResult] = Field(default_factory=list)
hosts: list[Host] = Field(default_factory=list)
nuclei_findings: list[NucleiFinding] = Field(default_factory=list)
triage: TriageReport | None = None
notice: str = Field(
default="Authorized use only. Recon-and-triage scope: this report enumerates and "
"references findings; it does not run or generate exploits.",
frozen=True,
)
def export_json_schema(out_dir: str | Path) -> Path:
"""Write the ReconReport + TriageReport JSON Schemas to ``out_dir``."""
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
recon_path = out / "recon_report.schema.json"
triage_path = out / "triage_report.schema.json"
recon_path.write_text(json.dumps(ReconReport.model_json_schema(), indent=2) + "\n")
triage_path.write_text(json.dumps(TriageReport.model_json_schema(), indent=2) + "\n")
return recon_path
if __name__ == "__main__": # pragma: no cover
import sys
target = sys.argv[1] if len(sys.argv) > 1 else "schemas"
print(export_json_schema(target))
+129
View File
@@ -0,0 +1,129 @@
"""Scope enforcement — mandatory gate. Anything not explicitly in scope is dropped.
A ``scope.yaml`` declares in-scope domains and CIDRs plus optional out-of-scope
exclusions. The tool refuses to touch anything not in scope: out-of-scope input is
dropped with a logged warning, never scanned.
"""
from __future__ import annotations
import ipaddress
import logging
from dataclasses import dataclass, field
from pathlib import Path
import yaml
from .schema import ScopeUsed
log = logging.getLogger("recon_triage.scope")
def _norm_domain(d: str) -> str:
return d.strip().lower().rstrip(".")
def _is_ip(value: str) -> bool:
try:
ipaddress.ip_address(value)
return True
except ValueError:
return False
@dataclass
class Scope:
"""Loaded scope rules with in/out gating.
Domain matching includes subdomains (``example.com`` matches ``api.example.com``).
Out-of-scope rules take precedence over in-scope rules.
"""
domains: list[str] = field(default_factory=list)
cidrs: list[str] = field(default_factory=list)
out_of_scope: list[str] = field(default_factory=list)
@classmethod
def load(cls, path: str | Path) -> Scope:
data = yaml.safe_load(Path(path).read_text()) or {}
return cls.from_dict(data)
@classmethod
def from_dict(cls, data: dict) -> Scope:
domains = [_norm_domain(d) for d in (data.get("in_scope_domains") or []) if d]
cidrs = [str(c).strip() for c in (data.get("in_scope_cidrs") or []) if c]
out = [str(o).strip().lower().rstrip(".") for o in (data.get("out_of_scope") or []) if o]
# Validate CIDRs early; drop invalid with a warning rather than crashing.
valid_cidrs = []
for c in cidrs:
try:
ipaddress.ip_network(c, strict=False)
valid_cidrs.append(c)
except ValueError:
log.warning("Ignoring invalid CIDR in scope: %s", c)
return cls(domains=domains, cidrs=valid_cidrs, out_of_scope=out)
# -- matching -----------------------------------------------------------
def _domain_in(self, host: str, rules: list[str]) -> bool:
host = _norm_domain(host)
for rule in rules:
if host == rule or host.endswith("." + rule):
return True
return False
def _ip_in_cidrs(self, ip: str) -> bool:
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return False
for c in self.cidrs:
try:
if addr in ipaddress.ip_network(c, strict=False):
return True
except ValueError:
continue
return False
def _ip_out(self, ip: str) -> bool:
for rule in self.out_of_scope:
if rule == ip:
return True
# out_of_scope may also carry CIDRs
try:
if ipaddress.ip_address(ip) in ipaddress.ip_network(rule, strict=False):
return True
except ValueError:
continue
return False
def is_in_scope(self, value: str) -> bool:
"""True iff ``value`` (a hostname or IP) is in scope and not excluded."""
value = value.strip()
if not value:
return False
if _is_ip(value):
if self._ip_out(value):
return False
return self._ip_in_cidrs(value)
# hostname
if self._domain_in(value, self.out_of_scope):
return False
return self._domain_in(value, self.domains)
def filter(self, values: list[str]) -> list[str]:
"""Return only in-scope values; log a warning for each dropped one."""
kept: list[str] = []
for v in values:
if self.is_in_scope(v):
kept.append(v)
else:
log.warning("Dropping out-of-scope target: %s", v)
return kept
def as_used(self) -> ScopeUsed:
return ScopeUsed(
in_scope_domains=list(self.domains),
in_scope_cidrs=list(self.cidrs),
out_of_scope=list(self.out_of_scope),
)