Update public source
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from .job_issuance import JobIssuanceService, IssuedJob
|
||||
from .job_result import JobResultError, JobResultService
|
||||
from .methods import MethodService, MethodServiceError
|
||||
|
||||
__all__ = [
|
||||
"IssuedJob",
|
||||
"JobIssuanceService",
|
||||
"JobResultError",
|
||||
"JobResultService",
|
||||
"MethodService",
|
||||
"MethodServiceError",
|
||||
]
|
||||
@@ -0,0 +1,348 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import yaml
|
||||
|
||||
from cracklab.server.repositories.jobs import JobsRepository
|
||||
from cracklab.server.repositories.methods import MethodsRepository
|
||||
from cracklab.server.services.method_definition import (
|
||||
validate_method_definition,
|
||||
)
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
CONFIG_FILE = PROJECT_ROOT / "config" / "config.yaml"
|
||||
|
||||
|
||||
class JobExportError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class JobExportService:
|
||||
def __init__(
|
||||
self,
|
||||
conn,
|
||||
timezone_name: str | None = None,
|
||||
) -> None:
|
||||
self._conn = conn
|
||||
self._jobs = JobsRepository(conn)
|
||||
self._methods = MethodsRepository(conn)
|
||||
self._timezone_name = (
|
||||
timezone_name
|
||||
if timezone_name is not None
|
||||
else self._load_timezone()
|
||||
)
|
||||
|
||||
try:
|
||||
self._timezone = ZoneInfo(self._timezone_name)
|
||||
except Exception as exc:
|
||||
raise JobExportError(
|
||||
f"Invalid timezone: {self._timezone_name}"
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def _load_timezone() -> str:
|
||||
if not CONFIG_FILE.is_file():
|
||||
raise JobExportError(
|
||||
f"Configuration file not found: {CONFIG_FILE}"
|
||||
)
|
||||
|
||||
try:
|
||||
with CONFIG_FILE.open(
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as fp:
|
||||
config = yaml.safe_load(fp) or {}
|
||||
except Exception as exc:
|
||||
raise JobExportError(
|
||||
f"Failed to load configuration: {CONFIG_FILE}"
|
||||
) from exc
|
||||
|
||||
timezone_name = config.get(
|
||||
"timezone",
|
||||
"Europe/Moscow",
|
||||
)
|
||||
|
||||
if not isinstance(timezone_name, str) or not timezone_name:
|
||||
raise JobExportError(
|
||||
"Configuration timezone must be a non-empty string"
|
||||
)
|
||||
|
||||
return timezone_name
|
||||
|
||||
@staticmethod
|
||||
def _parse_utc(text: str) -> datetime:
|
||||
try:
|
||||
value = datetime.fromisoformat(text)
|
||||
except ValueError as exc:
|
||||
raise JobExportError(
|
||||
f"Invalid job created_at: {text}"
|
||||
) from exc
|
||||
|
||||
if value.tzinfo is None:
|
||||
raise JobExportError(
|
||||
f"Job timestamp is not timezone-aware: {text}"
|
||||
)
|
||||
|
||||
value = value.astimezone(timezone.utc)
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _unique_hash_content(rows) -> tuple[str, int]:
|
||||
unique_hashes: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for row in rows:
|
||||
hash22000 = str(row["hash22000"]).strip()
|
||||
|
||||
if not hash22000:
|
||||
raise JobExportError(
|
||||
"Job contains an empty hash22000 value"
|
||||
)
|
||||
|
||||
if hash22000 in seen:
|
||||
continue
|
||||
|
||||
seen.add(hash22000)
|
||||
unique_hashes.append(hash22000)
|
||||
|
||||
content = "\n".join(unique_hashes) + "\n"
|
||||
|
||||
return content, len(unique_hashes)
|
||||
|
||||
@staticmethod
|
||||
def _sha256_text(content: str) -> str:
|
||||
return hashlib.sha256(
|
||||
content.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _json_bytes(data: dict) -> bytes:
|
||||
return (
|
||||
json.dumps(
|
||||
data,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
def export_job(
|
||||
self,
|
||||
job_id: str,
|
||||
output_dir: str | Path,
|
||||
) -> Path:
|
||||
job = self._jobs.get_job(job_id)
|
||||
|
||||
if job is None:
|
||||
raise JobExportError(
|
||||
f"Job not found: {job_id}"
|
||||
)
|
||||
|
||||
method_version = self._methods.get_method_version(
|
||||
str(job["method_id"]),
|
||||
int(job["method_version"]),
|
||||
)
|
||||
|
||||
if method_version is None:
|
||||
raise JobExportError(
|
||||
"Method version not found: "
|
||||
f"{job['method_id']} v{job['method_version']}"
|
||||
)
|
||||
|
||||
try:
|
||||
method_definition = json.loads(
|
||||
method_version["definition_json"]
|
||||
)
|
||||
validate_method_definition(
|
||||
method_definition
|
||||
)
|
||||
except (json.JSONDecodeError, TypeError, ValueError) as exc:
|
||||
raise JobExportError(
|
||||
"Invalid method version definition: "
|
||||
f"{job['method_id']} v{job['method_version']}"
|
||||
) from exc
|
||||
|
||||
hash_mode = int(
|
||||
method_definition["hash_mode"]
|
||||
)
|
||||
|
||||
handshake_rows = self._jobs.list_job_handshakes(
|
||||
job_id
|
||||
)
|
||||
step_rows = self._jobs.list_job_steps(
|
||||
job_id
|
||||
)
|
||||
|
||||
hash_content, hash_count = self._unique_hash_content(
|
||||
handshake_rows
|
||||
)
|
||||
|
||||
hash_sha256 = self._sha256_text(
|
||||
hash_content
|
||||
)
|
||||
|
||||
if hash_sha256 != job["hash_file_sha256"]:
|
||||
raise JobExportError(
|
||||
"Job hash snapshot SHA-256 mismatch: "
|
||||
f"expected {job['hash_file_sha256']}, "
|
||||
f"calculated {hash_sha256}"
|
||||
)
|
||||
|
||||
if hash_count != int(job["hash_count"]):
|
||||
raise JobExportError(
|
||||
"Job hash count mismatch: "
|
||||
f"expected {job['hash_count']}, "
|
||||
f"calculated {hash_count}"
|
||||
)
|
||||
|
||||
if len(handshake_rows) == 0:
|
||||
raise JobExportError(
|
||||
f"Job contains no handshake snapshot: {job_id}"
|
||||
)
|
||||
|
||||
if len(step_rows) != int(job["step_count"]):
|
||||
raise JobExportError(
|
||||
"Job step count mismatch: "
|
||||
f"expected {job['step_count']}, "
|
||||
f"calculated {len(step_rows)}"
|
||||
)
|
||||
|
||||
created_at = self._parse_utc(
|
||||
str(job["created_at"])
|
||||
)
|
||||
|
||||
local_created_at = created_at.astimezone(
|
||||
self._timezone
|
||||
)
|
||||
|
||||
zip_filename = (
|
||||
"JOB-"
|
||||
+ local_created_at.strftime(
|
||||
"%d-%m-%Y_%H%M%S"
|
||||
)
|
||||
+ "-"
|
||||
+ str(job["id"]).removeprefix("JOB-")
|
||||
+ ".zip"
|
||||
)
|
||||
|
||||
job_json = {
|
||||
"job_id": str(job["id"]),
|
||||
"created_at": str(job["created_at"]),
|
||||
"method_id": str(job["method_id"]),
|
||||
"method_version": int(job["method_version"]),
|
||||
"hash_mode": hash_mode,
|
||||
"hash_file_name": str(job["hash_file_name"]),
|
||||
"hash_file_sha256": str(job["hash_file_sha256"]),
|
||||
"hash_count": int(job["hash_count"]),
|
||||
"handshake_count": len(handshake_rows),
|
||||
"step_count": int(job["step_count"]),
|
||||
"handshakes": [
|
||||
{
|
||||
"handshake_id": int(row["handshake_id"]),
|
||||
"hash22000": str(row["hash22000"]),
|
||||
"access_point_id": (
|
||||
int(row["access_point_id"])
|
||||
if row["access_point_id"] is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
for row in handshake_rows
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step_no": int(row["step_no"]),
|
||||
"step_id": str(row["step_id"]),
|
||||
"session_name": str(row["session_name"]),
|
||||
"definition": json.loads(
|
||||
row["definition_json"]
|
||||
),
|
||||
}
|
||||
for row in step_rows
|
||||
],
|
||||
}
|
||||
|
||||
job_bytes = self._json_bytes(job_json)
|
||||
hash_bytes = hash_content.encode("utf-8")
|
||||
|
||||
manifest_files = [
|
||||
{
|
||||
"path": "job.json",
|
||||
"size": len(job_bytes),
|
||||
"sha256": hashlib.sha256(
|
||||
job_bytes
|
||||
).hexdigest(),
|
||||
},
|
||||
{
|
||||
"path": f"hashes/{job['hash_file_name']}",
|
||||
"size": len(hash_bytes),
|
||||
"sha256": hashlib.sha256(
|
||||
hash_bytes
|
||||
).hexdigest(),
|
||||
},
|
||||
]
|
||||
|
||||
manifest = {
|
||||
"format_version": 1,
|
||||
"job_id": str(job["id"]),
|
||||
"created_at": str(job["created_at"]),
|
||||
"zip_filename": zip_filename,
|
||||
"files": manifest_files,
|
||||
}
|
||||
|
||||
manifest_bytes = self._json_bytes(
|
||||
manifest
|
||||
)
|
||||
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
final_path = output_path / zip_filename
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="wb",
|
||||
suffix=".zip",
|
||||
dir=output_path,
|
||||
delete=False,
|
||||
) as tmp:
|
||||
temporary_path = Path(tmp.name)
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(
|
||||
temporary_path,
|
||||
mode="w",
|
||||
compression=zipfile.ZIP_DEFLATED,
|
||||
) as archive:
|
||||
archive.writestr(
|
||||
"job.json",
|
||||
job_bytes,
|
||||
)
|
||||
archive.writestr(
|
||||
f"hashes/{job['hash_file_name']}",
|
||||
hash_bytes,
|
||||
)
|
||||
archive.writestr(
|
||||
"manifest.json",
|
||||
manifest_bytes,
|
||||
)
|
||||
|
||||
temporary_path.replace(final_path)
|
||||
|
||||
except Exception:
|
||||
temporary_path.unlink(
|
||||
missing_ok=True
|
||||
)
|
||||
raise
|
||||
|
||||
return final_path
|
||||
@@ -0,0 +1,262 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from cracklab.server.repositories.audit import AuditRepository
|
||||
from cracklab.server.repositories.handshakes import HandshakesRepository
|
||||
from cracklab.server.repositories.jobs import JobsRepository
|
||||
from cracklab.server.repositories.methods import MethodsRepository
|
||||
from cracklab.server.repositories.reservations import ReservationsRepository
|
||||
from cracklab.server.services.method_definition import (
|
||||
MethodDefinitionError,
|
||||
validate_method_definition,
|
||||
)
|
||||
|
||||
|
||||
class JobIssuanceError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IssuedJob:
|
||||
job_id: str
|
||||
method_id: str
|
||||
method_version: int
|
||||
status: str
|
||||
created_at: str
|
||||
hash_file_name: str
|
||||
hash_file_sha256: str
|
||||
hash_count: int
|
||||
handshake_count: int
|
||||
step_count: int
|
||||
hash_content: str
|
||||
handshake_ids: tuple[int, ...]
|
||||
steps: tuple[dict[str, Any], ...]
|
||||
|
||||
|
||||
class JobIssuanceService:
|
||||
def __init__(self, conn: sqlite3.Connection) -> None:
|
||||
self._conn = conn
|
||||
self._methods = MethodsRepository(conn)
|
||||
self._handshakes = HandshakesRepository(conn)
|
||||
self._jobs = JobsRepository(conn)
|
||||
self._reservations = ReservationsRepository(conn)
|
||||
self._audit = AuditRepository(conn)
|
||||
|
||||
def issue_job(
|
||||
self,
|
||||
method_id: str,
|
||||
method_version: int,
|
||||
limit: int,
|
||||
client_id: str | None = None,
|
||||
) -> IssuedJob | None:
|
||||
if limit <= 0:
|
||||
raise ValueError("limit must be greater than zero")
|
||||
|
||||
method_version_row = self._methods.get_method_version(
|
||||
method_id,
|
||||
method_version,
|
||||
)
|
||||
if method_version_row is None:
|
||||
raise JobIssuanceError(
|
||||
f"Method version not found: {method_id} v{method_version}"
|
||||
)
|
||||
|
||||
definition = self._decode_method_definition(
|
||||
method_version_row["definition_json"],
|
||||
)
|
||||
steps = self._build_steps(
|
||||
method_id,
|
||||
method_version,
|
||||
definition,
|
||||
)
|
||||
|
||||
self._conn.execute("BEGIN IMMEDIATE")
|
||||
|
||||
try:
|
||||
handshake_rows = self._handshakes.list_eligible(
|
||||
method_id,
|
||||
method_version,
|
||||
limit,
|
||||
)
|
||||
|
||||
if not handshake_rows:
|
||||
self._conn.rollback()
|
||||
return None
|
||||
|
||||
job_id = f"JOB-{uuid.uuid4().hex[:12].upper()}"
|
||||
hash_file_name = f"{job_id}.22000"
|
||||
|
||||
handshake_ids = tuple(
|
||||
int(row["handshake_id"])
|
||||
for row in handshake_rows
|
||||
)
|
||||
|
||||
unique_hashes: list[str] = []
|
||||
seen_hashes: set[str] = set()
|
||||
|
||||
for row in handshake_rows:
|
||||
hash22000 = str(row["hash22000"]).strip()
|
||||
|
||||
if hash22000 not in seen_hashes:
|
||||
seen_hashes.add(hash22000)
|
||||
unique_hashes.append(hash22000)
|
||||
|
||||
hash_content = "\n".join(unique_hashes) + "\n"
|
||||
hash_file_sha256 = hashlib.sha256(
|
||||
hash_content.encode("utf-8"),
|
||||
).hexdigest()
|
||||
|
||||
created_at = self._jobs.create_job(
|
||||
job_id=job_id,
|
||||
method_id=method_id,
|
||||
method_version=method_version,
|
||||
status="QUEUED",
|
||||
hash_file_name=hash_file_name,
|
||||
hash_file_sha256=hash_file_sha256,
|
||||
hash_count=len(unique_hashes),
|
||||
step_count=len(steps),
|
||||
client_id=client_id,
|
||||
)
|
||||
|
||||
for row in handshake_rows:
|
||||
self._jobs.add_job_handshake(
|
||||
job_id=job_id,
|
||||
handshake_id=int(row["handshake_id"]),
|
||||
hash22000=str(row["hash22000"]).strip(),
|
||||
access_point_id=(
|
||||
int(row["access_point_id"])
|
||||
if row["access_point_id"] is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
self._reservations.create_reservation(
|
||||
reservation_id=uuid.uuid4().hex,
|
||||
handshake_id=int(row["handshake_id"]),
|
||||
method_id=method_id,
|
||||
method_version=method_version,
|
||||
job_id=job_id,
|
||||
client_id=client_id,
|
||||
)
|
||||
|
||||
for step_no, step in enumerate(steps, start=1):
|
||||
self._jobs.create_job_step(
|
||||
job_id=job_id,
|
||||
step_no=step_no,
|
||||
step_id=step["step_id"],
|
||||
status="PENDING",
|
||||
session_name=self._session_name(
|
||||
job_id,
|
||||
step["step_id"],
|
||||
),
|
||||
definition=step,
|
||||
)
|
||||
|
||||
self._audit.create_event(
|
||||
event_type="JOB_ISSUED",
|
||||
job_id=job_id,
|
||||
client_id=client_id,
|
||||
method_id=method_id,
|
||||
method_version=method_version,
|
||||
details={
|
||||
"handshake_count": len(handshake_rows),
|
||||
"hash_count": len(unique_hashes),
|
||||
"step_count": len(steps),
|
||||
"hash_file_name": hash_file_name,
|
||||
},
|
||||
)
|
||||
|
||||
self._conn.commit()
|
||||
|
||||
except Exception:
|
||||
self._conn.rollback()
|
||||
raise
|
||||
|
||||
return IssuedJob(
|
||||
job_id=job_id,
|
||||
method_id=method_id,
|
||||
method_version=method_version,
|
||||
status="QUEUED",
|
||||
created_at=created_at,
|
||||
hash_file_name=hash_file_name,
|
||||
hash_file_sha256=hash_file_sha256,
|
||||
hash_count=len(unique_hashes),
|
||||
handshake_count=len(handshake_rows),
|
||||
step_count=len(steps),
|
||||
hash_content=hash_content,
|
||||
handshake_ids=handshake_ids,
|
||||
steps=tuple(steps),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _decode_method_definition(
|
||||
definition_json: str,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
definition = json.loads(definition_json)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise JobIssuanceError(
|
||||
"Method version contains invalid definition JSON"
|
||||
) from exc
|
||||
|
||||
try:
|
||||
return validate_method_definition(definition)
|
||||
except MethodDefinitionError as exc:
|
||||
raise JobIssuanceError(str(exc)) from exc
|
||||
|
||||
@staticmethod
|
||||
def _build_steps(
|
||||
method_id: str,
|
||||
method_version: int,
|
||||
definition: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
del method_id
|
||||
del method_version
|
||||
|
||||
source_steps = definition["steps"]
|
||||
result: list[dict[str, Any]] = []
|
||||
seen_step_ids: set[str] = set()
|
||||
|
||||
for step in source_steps:
|
||||
if not isinstance(step, dict):
|
||||
raise JobIssuanceError(
|
||||
"Each method step must be a JSON object"
|
||||
)
|
||||
|
||||
step_id = step.get("step_id")
|
||||
|
||||
if not isinstance(step_id, str) or not step_id:
|
||||
raise JobIssuanceError(
|
||||
"Each method step must have a non-empty step_id"
|
||||
)
|
||||
|
||||
if step_id in seen_step_ids:
|
||||
raise JobIssuanceError(
|
||||
f"Duplicate step_id: {step_id}"
|
||||
)
|
||||
|
||||
seen_step_ids.add(step_id)
|
||||
|
||||
attack_mode = step.get("attack_mode")
|
||||
|
||||
if not isinstance(attack_mode, int) or attack_mode < 0:
|
||||
raise JobIssuanceError(
|
||||
f"Invalid attack_mode for step: {step_id}"
|
||||
)
|
||||
|
||||
result.append(dict(step))
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _session_name(
|
||||
job_id: str,
|
||||
step_id: str,
|
||||
) -> str:
|
||||
return f"cl_{job_id[:12]}_{step_id}"
|
||||
@@ -0,0 +1,740 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
import zipfile
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from cracklab.server.repositories.attempts import AttemptsRepository
|
||||
from cracklab.server.repositories.audit import AuditRepository
|
||||
from cracklab.server.repositories.credentials import CredentialsRepository
|
||||
from cracklab.server.repositories.jobs import JobsRepository
|
||||
from cracklab.server.repositories.reservations import ReservationsRepository
|
||||
from cracklab.server.repositories.results import ResultsRepository
|
||||
|
||||
|
||||
class JobResultError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class JobResultService:
|
||||
def __init__(self, conn) -> None:
|
||||
self._conn = conn
|
||||
self._attempts = AttemptsRepository(conn)
|
||||
self._audit = AuditRepository(conn)
|
||||
self._credentials = CredentialsRepository(conn)
|
||||
self._jobs = JobsRepository(conn)
|
||||
self._reservations = ReservationsRepository(conn)
|
||||
self._results = ResultsRepository(conn)
|
||||
|
||||
def import_result(self, package_path: Path | str) -> dict[str, Any]:
|
||||
package = Path(package_path)
|
||||
|
||||
if not package.is_file():
|
||||
raise JobResultError(f"Result package not found: {package}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="cracklab-result-") as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(package, "r") as zf:
|
||||
self._safe_extract(zf, root)
|
||||
except zipfile.BadZipFile as exc:
|
||||
raise JobResultError(
|
||||
f"Invalid result package: {package}"
|
||||
) from exc
|
||||
|
||||
result_path, show_path = self._find_result_files(root)
|
||||
result = self._read_json(result_path)
|
||||
|
||||
self._validate_result_sha256(result)
|
||||
|
||||
job_id = result.get("job_id")
|
||||
if not isinstance(job_id, str) or not job_id:
|
||||
raise JobResultError("Result is missing job_id")
|
||||
|
||||
self._conn.execute("BEGIN IMMEDIATE")
|
||||
|
||||
try:
|
||||
job = self._jobs.get_job(job_id)
|
||||
|
||||
if job is None:
|
||||
raise JobResultError(f"Unknown Job: {job_id}")
|
||||
|
||||
idempotent = self._check_idempotency(job, result)
|
||||
|
||||
if idempotent:
|
||||
self._conn.rollback()
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"status": "ALREADY_IMPORTED",
|
||||
"result_sha256": result["result_sha256"],
|
||||
}
|
||||
|
||||
self._validate_job_contract(job, result)
|
||||
|
||||
job_handshakes = self._load_job_handshakes(job_id)
|
||||
|
||||
unique_hashes = {
|
||||
row["hash22000"]
|
||||
for row in job_handshakes
|
||||
}
|
||||
|
||||
if len(unique_hashes) != job["hash_count"]:
|
||||
raise JobResultError(
|
||||
"Job hash_count does not match unique "
|
||||
"crack_job_handshakes hash22000 values"
|
||||
)
|
||||
|
||||
found = self._parse_show_file(
|
||||
show_path,
|
||||
job_handshakes,
|
||||
)
|
||||
|
||||
credential_stats = self._import_credentials(
|
||||
found,
|
||||
job_handshakes,
|
||||
)
|
||||
|
||||
attempt_count = self._register_attempts(
|
||||
job,
|
||||
result,
|
||||
job_handshakes,
|
||||
)
|
||||
|
||||
self._update_steps(job_id, result["steps"])
|
||||
|
||||
released = self._release_reservations(job_id)
|
||||
|
||||
imported_at = self._now()
|
||||
|
||||
self._jobs.update_job_status(
|
||||
job_id,
|
||||
"COMPLETED",
|
||||
)
|
||||
|
||||
self._jobs.update_job_result(
|
||||
job_id,
|
||||
result_sha256=result["result_sha256"],
|
||||
imported_at=imported_at,
|
||||
)
|
||||
|
||||
self._results.create_result(
|
||||
job_id,
|
||||
"COMPLETED",
|
||||
result,
|
||||
imported_at=imported_at,
|
||||
)
|
||||
|
||||
self._audit.create_event(
|
||||
event_type="JOB_RESULT_IMPORTED",
|
||||
job_id=job_id,
|
||||
client_id=job["client_id"],
|
||||
method_id=job["method_id"],
|
||||
method_version=job["method_version"],
|
||||
details={
|
||||
"result_sha256": result["result_sha256"],
|
||||
"credentials_imported": credential_stats["imported"],
|
||||
"credentials_already_present": (
|
||||
credential_stats["already_present"]
|
||||
),
|
||||
"attempts_completed": attempt_count,
|
||||
"reservations_released": released,
|
||||
},
|
||||
)
|
||||
|
||||
self._conn.commit()
|
||||
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"status": "COMPLETED",
|
||||
"result_sha256": result["result_sha256"],
|
||||
"credentials_imported": credential_stats["imported"],
|
||||
"credentials_already_present": (
|
||||
credential_stats["already_present"]
|
||||
),
|
||||
"attempts_completed": attempt_count,
|
||||
"reservations_released": released,
|
||||
}
|
||||
|
||||
except Exception:
|
||||
self._conn.rollback()
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _now() -> str:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
return datetime.now(timezone.utc).isoformat(
|
||||
timespec="milliseconds"
|
||||
).replace("+00:00", "Z")
|
||||
|
||||
@staticmethod
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise JobResultError(
|
||||
f"Invalid result.json: {path}"
|
||||
) from exc
|
||||
|
||||
if not isinstance(value, dict):
|
||||
raise JobResultError("result.json must contain a JSON object")
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _safe_extract(
|
||||
zf: zipfile.ZipFile,
|
||||
destination: Path,
|
||||
) -> None:
|
||||
destination = destination.resolve()
|
||||
|
||||
for member in zf.infolist():
|
||||
target = (destination / member.filename).resolve()
|
||||
|
||||
try:
|
||||
target.relative_to(destination)
|
||||
except ValueError as exc:
|
||||
raise JobResultError(
|
||||
f"Unsafe path in result package: {member.filename}"
|
||||
) from exc
|
||||
|
||||
if member.is_dir():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
continue
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zf.open(member) as src, target.open("wb") as dst:
|
||||
while True:
|
||||
chunk = src.read(1024 * 1024)
|
||||
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
dst.write(chunk)
|
||||
|
||||
@staticmethod
|
||||
def _find_result_files(
|
||||
root: Path,
|
||||
) -> tuple[Path, Path]:
|
||||
manifests = list(root.rglob("result.json"))
|
||||
|
||||
if len(manifests) != 1:
|
||||
raise JobResultError(
|
||||
"Result package must contain exactly one result.json"
|
||||
)
|
||||
|
||||
show_files = list(root.rglob("hashcat-show.txt"))
|
||||
|
||||
if len(show_files) != 1:
|
||||
raise JobResultError(
|
||||
"Result package must contain exactly one hashcat-show.txt"
|
||||
)
|
||||
|
||||
return manifests[0], show_files[0]
|
||||
|
||||
@staticmethod
|
||||
def _validate_result_sha256(result: dict[str, Any]) -> None:
|
||||
expected = result.get("result_sha256")
|
||||
|
||||
if not isinstance(expected, str) or not expected:
|
||||
raise JobResultError(
|
||||
"Result is missing result_sha256"
|
||||
)
|
||||
|
||||
payload = dict(result)
|
||||
payload.pop("result_sha256", None)
|
||||
|
||||
payload_bytes = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
).encode("utf-8")
|
||||
|
||||
actual = hashlib.sha256(payload_bytes).hexdigest()
|
||||
|
||||
if actual != expected:
|
||||
raise JobResultError(
|
||||
"Invalid result_sha256: "
|
||||
f"expected {expected}, calculated {actual}"
|
||||
)
|
||||
|
||||
def _check_idempotency(
|
||||
self,
|
||||
job,
|
||||
result: dict[str, Any],
|
||||
) -> bool:
|
||||
result_sha256 = result["result_sha256"]
|
||||
|
||||
stored_job_sha = job["result_sha256"]
|
||||
stored_result = self._results.get_result(job["id"])
|
||||
|
||||
if stored_job_sha is not None:
|
||||
if stored_job_sha != result_sha256:
|
||||
raise JobResultError(
|
||||
f"Job {job['id']} already has a different result_sha256"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
if stored_result is not None:
|
||||
stored_payload = self._results.decode_result(stored_result)
|
||||
stored_sha = stored_payload.get("result_sha256")
|
||||
|
||||
if stored_sha != result_sha256:
|
||||
raise JobResultError(
|
||||
f"Job {job['id']} already has a different imported result"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
if job["imported_at"] is not None:
|
||||
raise JobResultError(
|
||||
f"Job {job['id']} is marked imported without a stored result"
|
||||
)
|
||||
|
||||
if job["status"] == "COMPLETED":
|
||||
raise JobResultError(
|
||||
f"Job {job['id']} is already COMPLETED without an imported result"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def _validate_job_contract(
|
||||
self,
|
||||
job,
|
||||
result: dict[str, Any],
|
||||
) -> None:
|
||||
if result.get("schema_version") != 1:
|
||||
raise JobResultError(
|
||||
"Unsupported result schema_version: "
|
||||
f"{result.get('schema_version')!r}"
|
||||
)
|
||||
|
||||
if result.get("job_id") != job["id"]:
|
||||
raise JobResultError(
|
||||
"Result Job ID does not match the database Job"
|
||||
)
|
||||
|
||||
if result.get("status") != "COMPLETED":
|
||||
raise JobResultError(
|
||||
"Only COMPLETED results can be imported"
|
||||
)
|
||||
|
||||
if result.get("hash_file_sha256") != job["hash_file_sha256"]:
|
||||
raise JobResultError(
|
||||
"Result hash_file_sha256 does not match the Job"
|
||||
)
|
||||
|
||||
if result.get("hash_count") != job["hash_count"]:
|
||||
raise JobResultError(
|
||||
"Result hash_count does not match the Job"
|
||||
)
|
||||
|
||||
method = result.get("method")
|
||||
|
||||
if not isinstance(method, dict):
|
||||
raise JobResultError(
|
||||
"Result is missing method object"
|
||||
)
|
||||
|
||||
if method.get("id") != job["method_id"]:
|
||||
raise JobResultError(
|
||||
"Result method id does not match the Job"
|
||||
)
|
||||
|
||||
if method.get("version") != job["method_version"]:
|
||||
raise JobResultError(
|
||||
"Result method version does not match the Job"
|
||||
)
|
||||
|
||||
completed_at = result.get("completed_at")
|
||||
|
||||
if not isinstance(completed_at, str) or not completed_at:
|
||||
raise JobResultError(
|
||||
"Result is missing completed_at"
|
||||
)
|
||||
|
||||
result_steps = result.get("steps")
|
||||
|
||||
if not isinstance(result_steps, list):
|
||||
raise JobResultError(
|
||||
"Result is missing steps list"
|
||||
)
|
||||
|
||||
if len(result_steps) != job["step_count"]:
|
||||
raise JobResultError(
|
||||
"Result step count does not match the Job"
|
||||
)
|
||||
|
||||
db_steps = self._jobs.list_job_steps(job["id"])
|
||||
|
||||
if len(db_steps) != job["step_count"]:
|
||||
raise JobResultError(
|
||||
"Database Job step count does not match crack_job_steps"
|
||||
)
|
||||
|
||||
db_by_no = {
|
||||
row["step_no"]: row
|
||||
for row in db_steps
|
||||
}
|
||||
|
||||
seen_step_numbers: set[int] = set()
|
||||
|
||||
for step in result_steps:
|
||||
if not isinstance(step, dict):
|
||||
raise JobResultError(
|
||||
"Each result step must be an object"
|
||||
)
|
||||
|
||||
step_no = step.get("step_no")
|
||||
|
||||
if not isinstance(step_no, int):
|
||||
raise JobResultError(
|
||||
f"Invalid result step_no: {step_no!r}"
|
||||
)
|
||||
|
||||
if step_no in seen_step_numbers:
|
||||
raise JobResultError(
|
||||
f"Duplicate result step_no: {step_no}"
|
||||
)
|
||||
|
||||
seen_step_numbers.add(step_no)
|
||||
|
||||
db_step = db_by_no.get(step_no)
|
||||
|
||||
if db_step is None:
|
||||
raise JobResultError(
|
||||
f"Result contains unknown step_no: {step_no}"
|
||||
)
|
||||
|
||||
if step.get("step_id") != db_step["step_id"]:
|
||||
raise JobResultError(
|
||||
f"Step {step_no}: step_id does not match the Job"
|
||||
)
|
||||
|
||||
if step.get("session") != db_step["session_name"]:
|
||||
raise JobResultError(
|
||||
f"Step {step_no}: session does not match the Job"
|
||||
)
|
||||
|
||||
if step.get("status") != "COMPLETED":
|
||||
raise JobResultError(
|
||||
f"Step {step_no}: result status must be COMPLETED"
|
||||
)
|
||||
|
||||
def _load_job_handshakes(
|
||||
self,
|
||||
job_id: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
rows = self._jobs.list_job_handshakes(job_id)
|
||||
|
||||
if not rows:
|
||||
raise JobResultError(
|
||||
f"Job {job_id} contains no handshakes"
|
||||
)
|
||||
|
||||
result = []
|
||||
|
||||
for row in rows:
|
||||
result.append(
|
||||
{
|
||||
"handshake_id": row["handshake_id"],
|
||||
"hash22000": row["hash22000"],
|
||||
"access_point_id": row["access_point_id"],
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _normalize_mac(value: str) -> str:
|
||||
normalized = value.strip().lower().replace(":", "").replace("-", "")
|
||||
|
||||
if len(normalized) != 12:
|
||||
raise JobResultError(
|
||||
f"Invalid MAC address: {value!r}"
|
||||
)
|
||||
|
||||
try:
|
||||
int(normalized, 16)
|
||||
except ValueError as exc:
|
||||
raise JobResultError(
|
||||
f"Invalid MAC address: {value!r}"
|
||||
) from exc
|
||||
|
||||
return normalized
|
||||
|
||||
@classmethod
|
||||
def _hash_identity(
|
||||
cls,
|
||||
hash22000: str,
|
||||
) -> tuple[str, str, str]:
|
||||
parts = hash22000.split("*")
|
||||
|
||||
if len(parts) < 6 or parts[0] != "WPA":
|
||||
raise JobResultError(
|
||||
f"Unsupported hash22000 format: {hash22000}"
|
||||
)
|
||||
|
||||
if parts[1] not in {"01", "02"}:
|
||||
raise JobResultError(
|
||||
f"Unsupported WPA hash type: {parts[1]}"
|
||||
)
|
||||
|
||||
hash_value = parts[2].strip().lower()
|
||||
ap_mac = cls._normalize_mac(parts[3])
|
||||
client_mac = cls._normalize_mac(parts[4])
|
||||
|
||||
if not hash_value:
|
||||
raise JobResultError(
|
||||
f"Invalid hash22000 hash value: {hash22000}"
|
||||
)
|
||||
|
||||
return (
|
||||
hash_value,
|
||||
ap_mac,
|
||||
client_mac,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _parse_show_identity(
|
||||
cls,
|
||||
line: str,
|
||||
) -> tuple[str, str, str, str]:
|
||||
fields = line.split(":", 4)
|
||||
|
||||
if len(fields) != 5:
|
||||
raise JobResultError(
|
||||
f"Invalid Hashcat --show line: {line}"
|
||||
)
|
||||
|
||||
hash_value = fields[0].strip().lower()
|
||||
ap_mac = cls._normalize_mac(fields[1])
|
||||
client_mac = cls._normalize_mac(fields[2])
|
||||
password = fields[4]
|
||||
|
||||
if not hash_value:
|
||||
raise JobResultError(
|
||||
f"Invalid Hashcat --show hash: {line}"
|
||||
)
|
||||
|
||||
return (
|
||||
hash_value,
|
||||
ap_mac,
|
||||
client_mac,
|
||||
password,
|
||||
)
|
||||
|
||||
def _parse_show_file(
|
||||
self,
|
||||
path: Path,
|
||||
job_handshakes: list[dict[str, Any]],
|
||||
) -> list[tuple[str, str]]:
|
||||
identity_to_hashes: dict[
|
||||
tuple[str, str, str],
|
||||
list[str],
|
||||
] = defaultdict(list)
|
||||
|
||||
for row in job_handshakes:
|
||||
identity = self._hash_identity(row["hash22000"])
|
||||
hashes = identity_to_hashes[identity]
|
||||
|
||||
if row["hash22000"] not in hashes:
|
||||
hashes.append(row["hash22000"])
|
||||
|
||||
found: list[tuple[str, str]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
|
||||
for line_no, raw_line in enumerate(
|
||||
path.read_text(encoding="utf-8").splitlines(),
|
||||
start=1,
|
||||
):
|
||||
line = raw_line.strip()
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
hash_value, ap_mac, client_mac, password = (
|
||||
self._parse_show_identity(line)
|
||||
)
|
||||
|
||||
identity = (
|
||||
hash_value,
|
||||
ap_mac,
|
||||
client_mac,
|
||||
)
|
||||
|
||||
hashes = identity_to_hashes.get(identity)
|
||||
|
||||
if not hashes:
|
||||
raise JobResultError(
|
||||
"Result contains an unknown Hashcat --show line "
|
||||
f"{line_no}: {line}"
|
||||
)
|
||||
|
||||
for hash22000 in hashes:
|
||||
item = (hash22000, password)
|
||||
|
||||
if item not in seen:
|
||||
seen.add(item)
|
||||
found.append(item)
|
||||
|
||||
return found
|
||||
|
||||
def _import_credentials(
|
||||
self,
|
||||
found: list[tuple[str, str]],
|
||||
job_handshakes: list[dict[str, Any]],
|
||||
) -> dict[str, int]:
|
||||
by_hash: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
|
||||
for row in job_handshakes:
|
||||
by_hash[row["hash22000"]].append(row)
|
||||
|
||||
imported = 0
|
||||
already_present = 0
|
||||
|
||||
for hash22000, password in found:
|
||||
mappings = by_hash.get(hash22000)
|
||||
|
||||
if not mappings:
|
||||
raise JobResultError(
|
||||
"Hashcat --show contains a hash that is not part "
|
||||
f"of the Job: {hash22000}"
|
||||
)
|
||||
|
||||
for mapping in mappings:
|
||||
handshake_id = mapping["handshake_id"]
|
||||
access_point_id = mapping["access_point_id"]
|
||||
|
||||
if access_point_id is None:
|
||||
raise JobResultError(
|
||||
"Cannot import credential for handshake "
|
||||
f"{handshake_id}: access_point_id is NULL"
|
||||
)
|
||||
|
||||
existing = self._credentials.get_credential(
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
)
|
||||
|
||||
if existing is not None:
|
||||
already_present += 1
|
||||
self._credentials.mark_access_point_cracked(
|
||||
access_point_id,
|
||||
)
|
||||
continue
|
||||
|
||||
self._credentials.create_hashcat_credential(
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
)
|
||||
imported += 1
|
||||
|
||||
return {
|
||||
"imported": imported,
|
||||
"already_present": already_present,
|
||||
}
|
||||
|
||||
def _register_attempts(
|
||||
self,
|
||||
job,
|
||||
result: dict[str, Any],
|
||||
job_handshakes: list[dict[str, Any]],
|
||||
) -> int:
|
||||
if result["status"] != "COMPLETED":
|
||||
return 0
|
||||
|
||||
completed_at = result["completed_at"]
|
||||
count = 0
|
||||
|
||||
for row in job_handshakes:
|
||||
existing = self._attempts.get_attempt(
|
||||
row["handshake_id"],
|
||||
job["method_id"],
|
||||
job["method_version"],
|
||||
)
|
||||
|
||||
if existing is not None:
|
||||
if (
|
||||
existing["job_id"] != job["id"]
|
||||
or existing["status"] != "COMPLETED"
|
||||
):
|
||||
raise JobResultError(
|
||||
"Handshake "
|
||||
f"{row['handshake_id']} already has a "
|
||||
"conflicting attempt for this method/version"
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
self._attempts.create_attempt(
|
||||
handshake_id=row["handshake_id"],
|
||||
method_id=job["method_id"],
|
||||
method_version=job["method_version"],
|
||||
job_id=job["id"],
|
||||
completed_at=completed_at,
|
||||
)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def _update_steps(
|
||||
self,
|
||||
job_id: str,
|
||||
result_steps: list[dict[str, Any]],
|
||||
) -> None:
|
||||
for step in result_steps:
|
||||
step_no = step["step_no"]
|
||||
|
||||
self._jobs.update_job_step(
|
||||
job_id=job_id,
|
||||
step_no=step_no,
|
||||
status="COMPLETED",
|
||||
started_at=step.get("started_at"),
|
||||
completed_at=step.get("completed_at"),
|
||||
exit_code=step.get("exit_code"),
|
||||
restore_seen=(
|
||||
1 if step.get("restore_seen") else 0
|
||||
),
|
||||
error=step.get("error"),
|
||||
)
|
||||
|
||||
updated = self._jobs.get_job_step(
|
||||
job_id,
|
||||
step_no,
|
||||
)
|
||||
|
||||
if updated is None:
|
||||
raise JobResultError(
|
||||
f"Job step disappeared during result import: "
|
||||
f"{step_no}"
|
||||
)
|
||||
|
||||
def _release_reservations(
|
||||
self,
|
||||
job_id: str,
|
||||
) -> int:
|
||||
released = 0
|
||||
|
||||
for reservation in self._reservations.list_job_reservations(
|
||||
job_id
|
||||
):
|
||||
if reservation["status"] != "ACTIVE":
|
||||
continue
|
||||
|
||||
self._reservations.update_status(
|
||||
reservation["id"],
|
||||
"RELEASED",
|
||||
)
|
||||
released += 1
|
||||
|
||||
return released
|
||||
@@ -0,0 +1,251 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
class MethodDefinitionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
_RESOURCE_RE = re.compile(r"^[a-z][a-z0-9_-]*:[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||
_STEP_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||
_MASK_CHARSET_SIZES = {
|
||||
"?l": 26,
|
||||
"?u": 26,
|
||||
"?d": 10,
|
||||
"?s": 33,
|
||||
"?a": 95,
|
||||
"?b": 256,
|
||||
}
|
||||
_MASK_MIN_LENGTH = 8
|
||||
|
||||
|
||||
def validate_method_definition(
|
||||
definition: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
if not isinstance(definition, dict):
|
||||
raise MethodDefinitionError(
|
||||
"Method definition must be a JSON object"
|
||||
)
|
||||
|
||||
if definition.get("schema_version") != 1:
|
||||
raise MethodDefinitionError(
|
||||
"Unsupported method definition schema version"
|
||||
)
|
||||
|
||||
hash_mode = definition.get("hash_mode")
|
||||
|
||||
if not isinstance(hash_mode, int) or isinstance(hash_mode, bool):
|
||||
raise MethodDefinitionError(
|
||||
"Method definition hash_mode must be a non-negative integer"
|
||||
)
|
||||
|
||||
if hash_mode < 0:
|
||||
raise MethodDefinitionError(
|
||||
"Method definition hash_mode must be a non-negative integer"
|
||||
)
|
||||
|
||||
steps = definition.get("steps")
|
||||
|
||||
if not isinstance(steps, list) or not steps:
|
||||
raise MethodDefinitionError(
|
||||
"Method definition must contain a non-empty steps list"
|
||||
)
|
||||
|
||||
seen_step_ids: set[str] = set()
|
||||
|
||||
for step in steps:
|
||||
_validate_step(step, seen_step_ids)
|
||||
|
||||
return definition
|
||||
|
||||
|
||||
def _validate_step(
|
||||
step: Any,
|
||||
seen_step_ids: set[str],
|
||||
) -> None:
|
||||
if not isinstance(step, dict):
|
||||
raise MethodDefinitionError(
|
||||
"Each method step must be a JSON object"
|
||||
)
|
||||
|
||||
step_id = step.get("step_id")
|
||||
|
||||
if not isinstance(step_id, str) or not _STEP_ID_RE.fullmatch(step_id):
|
||||
raise MethodDefinitionError(
|
||||
"Each method step must have a valid step_id"
|
||||
)
|
||||
|
||||
if step_id in seen_step_ids:
|
||||
raise MethodDefinitionError(
|
||||
f"Duplicate step_id: {step_id}"
|
||||
)
|
||||
|
||||
seen_step_ids.add(step_id)
|
||||
|
||||
attack_mode = step.get("attack_mode")
|
||||
|
||||
if not isinstance(attack_mode, int) or isinstance(attack_mode, bool):
|
||||
raise MethodDefinitionError(
|
||||
f"Invalid attack_mode for step: {step_id}"
|
||||
)
|
||||
|
||||
if attack_mode < 0:
|
||||
raise MethodDefinitionError(
|
||||
f"Invalid attack_mode for step: {step_id}"
|
||||
)
|
||||
|
||||
if attack_mode == 0:
|
||||
_require_dictionary(step, step_id)
|
||||
|
||||
elif attack_mode == 1:
|
||||
_require_dictionaries(step, step_id)
|
||||
|
||||
elif attack_mode == 3:
|
||||
_require_mask(
|
||||
step,
|
||||
step_id,
|
||||
minimum_length=_MASK_MIN_LENGTH,
|
||||
)
|
||||
|
||||
elif attack_mode == 6:
|
||||
_require_dictionary(step, step_id)
|
||||
_require_mask(
|
||||
step,
|
||||
step_id,
|
||||
minimum_length=_MASK_MIN_LENGTH,
|
||||
)
|
||||
|
||||
elif attack_mode == 7:
|
||||
_require_mask(
|
||||
step,
|
||||
step_id,
|
||||
minimum_length=_MASK_MIN_LENGTH,
|
||||
)
|
||||
_require_dictionary(step, step_id)
|
||||
|
||||
if "rules" in step:
|
||||
_validate_resource_object(
|
||||
step["rules"],
|
||||
"rules",
|
||||
step_id,
|
||||
)
|
||||
|
||||
|
||||
def _require_dictionary(
|
||||
step: dict[str, Any],
|
||||
step_id: str,
|
||||
) -> None:
|
||||
if "dictionary" not in step:
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} requires dictionary"
|
||||
)
|
||||
|
||||
_validate_resource_object(
|
||||
step["dictionary"],
|
||||
"dictionary",
|
||||
step_id,
|
||||
)
|
||||
|
||||
|
||||
def _require_dictionaries(
|
||||
step: dict[str, Any],
|
||||
step_id: str,
|
||||
) -> None:
|
||||
dictionaries = step.get("dictionaries")
|
||||
|
||||
if not isinstance(dictionaries, list) or len(dictionaries) != 2:
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} requires exactly two dictionaries"
|
||||
)
|
||||
|
||||
for dictionary in dictionaries:
|
||||
_validate_resource_object(
|
||||
dictionary,
|
||||
"dictionary",
|
||||
step_id,
|
||||
)
|
||||
|
||||
|
||||
def _require_mask(
|
||||
step: dict[str, Any],
|
||||
step_id: str,
|
||||
minimum_length: int | None = None,
|
||||
) -> None:
|
||||
mask = step.get("mask")
|
||||
|
||||
if not isinstance(mask, str) or not mask:
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} requires a non-empty mask"
|
||||
)
|
||||
|
||||
length = 0
|
||||
index = 0
|
||||
|
||||
while index < len(mask):
|
||||
if mask[index] != "?":
|
||||
length += 1
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if index + 1 >= len(mask):
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} mask contains a lone '?' "
|
||||
f"at position {index + 1}"
|
||||
)
|
||||
|
||||
token = mask[index:index + 2]
|
||||
|
||||
if token == "??":
|
||||
length += 1
|
||||
index += 2
|
||||
continue
|
||||
|
||||
if token not in _MASK_CHARSET_SIZES:
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} contains unsupported mask token "
|
||||
f"{token!r} at position {index + 1}"
|
||||
)
|
||||
|
||||
length += 1
|
||||
index += 2
|
||||
|
||||
if (
|
||||
minimum_length is not None
|
||||
and length < minimum_length
|
||||
):
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} mask must produce at least "
|
||||
f"{minimum_length} characters; got {length}"
|
||||
)
|
||||
|
||||
def _validate_resource_object(
|
||||
value: Any,
|
||||
resource_type: str,
|
||||
step_id: str,
|
||||
) -> None:
|
||||
if not isinstance(value, dict):
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} {resource_type} must be an object"
|
||||
)
|
||||
|
||||
resource = value.get("resource")
|
||||
|
||||
if not isinstance(resource, str) or not _RESOURCE_RE.fullmatch(resource):
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} {resource_type}.resource must be "
|
||||
"a valid logical resource ID"
|
||||
)
|
||||
|
||||
expected_prefix = (
|
||||
"wordlist:"
|
||||
if resource_type == "dictionary"
|
||||
else f"{resource_type}:"
|
||||
)
|
||||
|
||||
if not resource.startswith(expected_prefix):
|
||||
raise MethodDefinitionError(
|
||||
f"Step {step_id} {resource_type}.resource must start "
|
||||
f"with {expected_prefix!r}"
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from cracklab.server.repositories.methods import MethodsRepository
|
||||
from cracklab.server.services.method_definition import (
|
||||
MethodDefinitionError,
|
||||
validate_method_definition,
|
||||
)
|
||||
|
||||
|
||||
class MethodServiceError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class MethodService:
|
||||
def __init__(self, conn: sqlite3.Connection) -> None:
|
||||
self._methods = MethodsRepository(conn)
|
||||
|
||||
def create_method(
|
||||
self,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
method_id: str | None = None,
|
||||
) -> str:
|
||||
if not name or not name.strip():
|
||||
raise MethodServiceError("Method name must not be empty")
|
||||
|
||||
if method_id is None:
|
||||
method_id = f"MTH-{uuid.uuid4().hex}"
|
||||
|
||||
if not method_id:
|
||||
raise MethodServiceError("Method ID must not be empty")
|
||||
|
||||
self._methods.create_method(
|
||||
method_id=method_id,
|
||||
name=name.strip(),
|
||||
description=description.strip() if description else None,
|
||||
)
|
||||
|
||||
return method_id
|
||||
|
||||
def create_method_version(
|
||||
self,
|
||||
method_id: str,
|
||||
version: int,
|
||||
definition: dict[str, Any],
|
||||
) -> None:
|
||||
if version <= 0:
|
||||
raise MethodServiceError(
|
||||
"Method version must be greater than zero"
|
||||
)
|
||||
|
||||
try:
|
||||
validate_method_definition(definition)
|
||||
except MethodDefinitionError as exc:
|
||||
raise MethodServiceError(str(exc)) from exc
|
||||
|
||||
self._methods.create_method_version(
|
||||
method_id=method_id,
|
||||
version=version,
|
||||
definition=definition,
|
||||
)
|
||||
|
||||
def create_next_method_version(
|
||||
self,
|
||||
method_id: str,
|
||||
definition: dict[str, Any],
|
||||
) -> int:
|
||||
try:
|
||||
validate_method_definition(definition)
|
||||
except MethodDefinitionError as exc:
|
||||
raise MethodServiceError(str(exc)) from exc
|
||||
|
||||
version = self._methods.get_next_method_version(method_id)
|
||||
|
||||
self._methods.create_method_version(
|
||||
method_id=method_id,
|
||||
version=version,
|
||||
definition=definition,
|
||||
)
|
||||
|
||||
return version
|
||||
|
||||
def get_method(self, method_id: str) -> sqlite3.Row | None:
|
||||
return self._methods.get_method(method_id)
|
||||
|
||||
def list_methods(self) -> list[sqlite3.Row]:
|
||||
return self._methods.list_methods()
|
||||
|
||||
def get_method_version(
|
||||
self,
|
||||
method_id: str,
|
||||
version: int,
|
||||
) -> sqlite3.Row | None:
|
||||
return self._methods.get_method_version(
|
||||
method_id,
|
||||
version,
|
||||
)
|
||||
|
||||
def list_method_versions(
|
||||
self,
|
||||
method_id: str,
|
||||
) -> list[sqlite3.Row]:
|
||||
return self._methods.list_method_versions(method_id)
|
||||
|
||||
def get_latest_method_version(
|
||||
self,
|
||||
method_id: str,
|
||||
) -> sqlite3.Row | None:
|
||||
versions = self._methods.list_method_versions(method_id)
|
||||
|
||||
if not versions:
|
||||
return None
|
||||
|
||||
return versions[-1]
|
||||
@@ -0,0 +1,914 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from cracklab.server.services.methods import MethodService
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StockMethod:
|
||||
method_id: str
|
||||
name: str
|
||||
description: str
|
||||
definition: dict[str, Any] | None
|
||||
|
||||
|
||||
STOCK_METHODS = (
|
||||
StockMethod(
|
||||
method_id="mobile_oper_spb_plus_79",
|
||||
name="MobileOperSpb+79",
|
||||
description="Перебор мобильных номеров Санкт-Петербурга с префиксом +79.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"+791100?d?d?d?d?d",
|
||||
"+791101?d?d?d?d?d",
|
||||
"+791102?d?d?d?d?d",
|
||||
"+791103?d?d?d?d?d",
|
||||
"+791108?d?d?d?d?d",
|
||||
"+791109?d?d?d?d?d",
|
||||
"+791110?d?d?d?d?d",
|
||||
"+791111?d?d?d?d?d",
|
||||
"+791112?d?d?d?d?d",
|
||||
"+791117?d?d?d?d?d",
|
||||
"+791119?d?d?d?d?d",
|
||||
"+79811?d?d?d?d?d?d",
|
||||
"+79817?d?d?d?d?d?d",
|
||||
"+79818?d?d?d?d?d?d",
|
||||
"+79819?d?d?d?d?d?d",
|
||||
"+798986?d?d?d?d?d",
|
||||
"+798686?d?d?d?d?d",
|
||||
"+798687?d?d?d?d?d",
|
||||
"+798688?d?d?d?d?d",
|
||||
"+792118?d?d?d?d?d",
|
||||
"+792119?d?d?d?d?d",
|
||||
"+79213?d?d?d?d?d?d",
|
||||
"+79214?d?d?d?d?d?d",
|
||||
"+79215?d?d?d?d?d?d",
|
||||
"+79216?d?d?d?d?d?d",
|
||||
"+79217?d?d?d?d?d?d",
|
||||
"+79218?d?d?d?d?d?d",
|
||||
"+79219?d?d?d?d?d?d",
|
||||
"+793110?d?d?d?d?d",
|
||||
"+793111?d?d?d?d?d",
|
||||
"+793112?d?d?d?d?d",
|
||||
"+793113?d?d?d?d?d",
|
||||
"+79312?d?d?d?d?d?d",
|
||||
"+79313?d?d?d?d?d?d",
|
||||
"+79315?d?d?d?d?d?d",
|
||||
"+79319?d?d?d?d?d?d",
|
||||
"+79291?d?d?d?d?d?d",
|
||||
"+799923?d?d?d?d?d",
|
||||
"+799924?d?d?d?d?d",
|
||||
"+799925?d?d?d?d?d",
|
||||
"+7963241?d?d?d?d",
|
||||
"+7963242?d?d?d?d",
|
||||
"+7963243?d?d?d?d",
|
||||
"+7963244?d?d?d?d",
|
||||
"+7963245?d?d?d?d",
|
||||
"+7963246?d?d?d?d",
|
||||
"+7963247?d?d?d?d",
|
||||
"+7963248?d?d?d?d",
|
||||
"+7963249?d?d?d?d",
|
||||
"+796330?d?d?d?d?d",
|
||||
"+796331?d?d?d?d?d",
|
||||
"+796332?d?d?d?d?d",
|
||||
"+796334?d?d?d?d?d",
|
||||
"+79650?d?d?d?d?d?d",
|
||||
"+79651?d?d?d?d?d?d",
|
||||
"+79652?d?d?d?d?d?d",
|
||||
"+79653?d?d?d?d?d?d",
|
||||
"+79654?d?d?d?d?d?d",
|
||||
"+79655?d?d?d?d?d?d",
|
||||
"+79656?d?d?d?d?d?d",
|
||||
"+79657?d?d?d?d?d?d",
|
||||
"+79658?d?d?d?d?d?d",
|
||||
"+79659?d?d?d?d?d?d",
|
||||
"+790520?d?d?d?d?d",
|
||||
"+790521?d?d?d?d?d",
|
||||
"+790522?d?d?d?d?d",
|
||||
"+790523?d?d?d?d?d",
|
||||
"+790524?d?d?d?d?d",
|
||||
"+790525?d?d?d?d?d",
|
||||
"+790526?d?d?d?d?d",
|
||||
"+790527?d?d?d?d?d",
|
||||
"+790528?d?d?d?d?d",
|
||||
"+790624?d?d?d?d?d",
|
||||
"+790625?d?d?d?d?d",
|
||||
"+790626?d?d?d?d?d",
|
||||
"+790627?d?d?d?d?d",
|
||||
"+790958?d?d?d?d?d",
|
||||
"+790959?d?d?d?d?d",
|
||||
"+796023?d?d?d?d?d",
|
||||
"+796024?d?d?d?d?d",
|
||||
"+796025?d?d?d?d?d",
|
||||
"+796026?d?d?d?d?d",
|
||||
"+796027?d?d?d?d?d",
|
||||
"+796028?d?d?d?d?d",
|
||||
"+796434?d?d?d?d?d",
|
||||
"+79500?d?d?d?d?d?d",
|
||||
"+79502?d?d?d?d?d?d",
|
||||
"+79516?d?d?d?d?d?d",
|
||||
"+79517?d?d?d?d?d?d",
|
||||
"+79518?d?d?d?d?d?d",
|
||||
"+79522?d?d?d?d?d?d",
|
||||
"+79523?d?d?d?d?d?d",
|
||||
"+79531?d?d?d?d?d?d",
|
||||
"+79533?d?d?d?d?d?d",
|
||||
"+790433?d?d?d?d?d",
|
||||
"+790451?d?d?d?d?d",
|
||||
"+790455?d?d?d?d?d",
|
||||
"+790460?d?d?d?d?d",
|
||||
"+790461?d?d?d?d?d",
|
||||
"+790462?d?d?d?d?d",
|
||||
"+790463?d?d?d?d?d",
|
||||
"+790464?d?d?d?d?d",
|
||||
"+799100?d?d?d?d?d",
|
||||
"+799101?d?d?d?d?d",
|
||||
"+799102?d?d?d?d?d",
|
||||
"+790062?d?d?d?d?d",
|
||||
"+790063?d?d?d?d?d",
|
||||
"+790064?d?d?d?d?d",
|
||||
"+790065?d?d?d?d?d",
|
||||
"+799900?d?d?d?d?d",
|
||||
"+799901?d?d?d?d?d",
|
||||
"+799902?d?d?d?d?d",
|
||||
"+799903?d?d?d?d?d",
|
||||
"+799904?d?d?d?d?d",
|
||||
"+799909?d?d?d?d?d",
|
||||
"+79991?d?d?d?d?d?d",
|
||||
"+79992?d?d?d?d?d?d",
|
||||
"+799322?d?d?d?d?d",
|
||||
"+799343?d?d?d?d?d",
|
||||
"+799344?d?d?d?d?d",
|
||||
"+799555?d?d?d?d?d",
|
||||
"+799559?d?d?d?d?d",
|
||||
"+799560?d?d?d?d?d",
|
||||
"+799590?d?d?d?d?d",
|
||||
"+795840?d?d?d?d?d",
|
||||
"+795856?d?d?d?d?d",
|
||||
"+799511?d?d?d?d?d",
|
||||
"+799512?d?d?d?d?d",
|
||||
"+7999944?d?d?d?d",
|
||||
"+7999945?d?d?d?d",
|
||||
"+7999946?d?d?d?d",
|
||||
"+7999947?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_spb_89",
|
||||
name="MobileOperSpb_89",
|
||||
description="Перебор мобильных номеров Санкт-Петербурга с префиксом 89.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"891100?d?d?d?d?d",
|
||||
"891101?d?d?d?d?d",
|
||||
"891102?d?d?d?d?d",
|
||||
"891103?d?d?d?d?d",
|
||||
"891108?d?d?d?d?d",
|
||||
"891109?d?d?d?d?d",
|
||||
"891110?d?d?d?d?d",
|
||||
"891111?d?d?d?d?d",
|
||||
"891112?d?d?d?d?d",
|
||||
"891117?d?d?d?d?d",
|
||||
"891119?d?d?d?d?d",
|
||||
"89811?d?d?d?d?d?d",
|
||||
"89817?d?d?d?d?d?d",
|
||||
"89818?d?d?d?d?d?d",
|
||||
"89819?d?d?d?d?d?d",
|
||||
"898986?d?d?d?d?d",
|
||||
"898686?d?d?d?d?d",
|
||||
"898687?d?d?d?d?d",
|
||||
"898688?d?d?d?d?d",
|
||||
"892118?d?d?d?d?d",
|
||||
"892119?d?d?d?d?d",
|
||||
"89213?d?d?d?d?d?d",
|
||||
"89214?d?d?d?d?d?d",
|
||||
"89215?d?d?d?d?d?d",
|
||||
"89216?d?d?d?d?d?d",
|
||||
"89217?d?d?d?d?d?d",
|
||||
"89218?d?d?d?d?d?d",
|
||||
"89219?d?d?d?d?d?d",
|
||||
"893110?d?d?d?d?d",
|
||||
"893111?d?d?d?d?d",
|
||||
"893112?d?d?d?d?d",
|
||||
"893113?d?d?d?d?d",
|
||||
"89312?d?d?d?d?d?d",
|
||||
"89313?d?d?d?d?d?d",
|
||||
"89315?d?d?d?d?d?d",
|
||||
"89319?d?d?d?d?d?d",
|
||||
"89291?d?d?d?d?d?d",
|
||||
"899923?d?d?d?d?d",
|
||||
"899924?d?d?d?d?d",
|
||||
"899925?d?d?d?d?d",
|
||||
"8963241?d?d?d?d",
|
||||
"8963242?d?d?d?d",
|
||||
"8963243?d?d?d?d",
|
||||
"8963244?d?d?d?d",
|
||||
"8963245?d?d?d?d",
|
||||
"8963246?d?d?d?d",
|
||||
"8963247?d?d?d?d",
|
||||
"8963248?d?d?d?d",
|
||||
"8963249?d?d?d?d",
|
||||
"896330?d?d?d?d?d",
|
||||
"896331?d?d?d?d?d",
|
||||
"896332?d?d?d?d?d",
|
||||
"896334?d?d?d?d?d",
|
||||
"89650?d?d?d?d?d?d",
|
||||
"89651?d?d?d?d?d?d",
|
||||
"89652?d?d?d?d?d?d",
|
||||
"89653?d?d?d?d?d?d",
|
||||
"89654?d?d?d?d?d?d",
|
||||
"89655?d?d?d?d?d?d",
|
||||
"89656?d?d?d?d?d?d",
|
||||
"89657?d?d?d?d?d?d",
|
||||
"89658?d?d?d?d?d?d",
|
||||
"89659?d?d?d?d?d?d",
|
||||
"890520?d?d?d?d?d",
|
||||
"890521?d?d?d?d?d",
|
||||
"890522?d?d?d?d?d",
|
||||
"890523?d?d?d?d?d",
|
||||
"890524?d?d?d?d?d",
|
||||
"890525?d?d?d?d?d",
|
||||
"890526?d?d?d?d?d",
|
||||
"890527?d?d?d?d?d",
|
||||
"890528?d?d?d?d?d",
|
||||
"890624?d?d?d?d?d",
|
||||
"890625?d?d?d?d?d",
|
||||
"890626?d?d?d?d?d",
|
||||
"890627?d?d?d?d?d",
|
||||
"890958?d?d?d?d?d",
|
||||
"890959?d?d?d?d?d",
|
||||
"896023?d?d?d?d?d",
|
||||
"896024?d?d?d?d?d",
|
||||
"896025?d?d?d?d?d",
|
||||
"896026?d?d?d?d?d",
|
||||
"896027?d?d?d?d?d",
|
||||
"896028?d?d?d?d?d",
|
||||
"896434?d?d?d?d?d",
|
||||
"89500?d?d?d?d?d?d",
|
||||
"89502?d?d?d?d?d?d",
|
||||
"89516?d?d?d?d?d?d",
|
||||
"89517?d?d?d?d?d?d",
|
||||
"89518?d?d?d?d?d?d",
|
||||
"89522?d?d?d?d?d?d",
|
||||
"89523?d?d?d?d?d?d",
|
||||
"89531?d?d?d?d?d?d",
|
||||
"89533?d?d?d?d?d?d",
|
||||
"890433?d?d?d?d?d",
|
||||
"890451?d?d?d?d?d",
|
||||
"890455?d?d?d?d?d",
|
||||
"890460?d?d?d?d?d",
|
||||
"890461?d?d?d?d?d",
|
||||
"890462?d?d?d?d?d",
|
||||
"890463?d?d?d?d?d",
|
||||
"890464?d?d?d?d?d",
|
||||
"899100?d?d?d?d?d",
|
||||
"899101?d?d?d?d?d",
|
||||
"899102?d?d?d?d?d",
|
||||
"890062?d?d?d?d?d",
|
||||
"890063?d?d?d?d?d",
|
||||
"890064?d?d?d?d?d",
|
||||
"890065?d?d?d?d?d",
|
||||
"899900?d?d?d?d?d",
|
||||
"899901?d?d?d?d?d",
|
||||
"899902?d?d?d?d?d",
|
||||
"899903?d?d?d?d?d",
|
||||
"899904?d?d?d?d?d",
|
||||
"899909?d?d?d?d?d",
|
||||
"89991?d?d?d?d?d?d",
|
||||
"89992?d?d?d?d?d?d",
|
||||
"899322?d?d?d?d?d",
|
||||
"899343?d?d?d?d?d",
|
||||
"899344?d?d?d?d?d",
|
||||
"899555?d?d?d?d?d",
|
||||
"899559?d?d?d?d?d",
|
||||
"899560?d?d?d?d?d",
|
||||
"899590?d?d?d?d?d",
|
||||
"895840?d?d?d?d?d",
|
||||
"895856?d?d?d?d?d",
|
||||
"899511?d?d?d?d?d",
|
||||
"899512?d?d?d?d?d",
|
||||
"8999944?d?d?d?d",
|
||||
"8999945?d?d?d?d",
|
||||
"8999946?d?d?d?d",
|
||||
"8999947?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_spb_79",
|
||||
name="MobileOperSpb_79",
|
||||
description="Перебор мобильных номеров Санкт-Петербурга без знака +.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"791100?d?d?d?d?d",
|
||||
"791101?d?d?d?d?d",
|
||||
"791102?d?d?d?d?d",
|
||||
"791103?d?d?d?d?d",
|
||||
"791108?d?d?d?d?d",
|
||||
"791109?d?d?d?d?d",
|
||||
"791110?d?d?d?d?d",
|
||||
"791111?d?d?d?d?d",
|
||||
"791112?d?d?d?d?d",
|
||||
"791117?d?d?d?d?d",
|
||||
"791119?d?d?d?d?d",
|
||||
"79811?d?d?d?d?d?d",
|
||||
"79817?d?d?d?d?d?d",
|
||||
"79818?d?d?d?d?d?d",
|
||||
"79819?d?d?d?d?d?d",
|
||||
"798986?d?d?d?d?d",
|
||||
"798686?d?d?d?d?d",
|
||||
"798687?d?d?d?d?d",
|
||||
"798688?d?d?d?d?d",
|
||||
"792118?d?d?d?d?d",
|
||||
"792119?d?d?d?d?d",
|
||||
"79213?d?d?d?d?d?d",
|
||||
"79214?d?d?d?d?d?d",
|
||||
"79215?d?d?d?d?d?d",
|
||||
"79216?d?d?d?d?d?d",
|
||||
"79217?d?d?d?d?d?d",
|
||||
"79218?d?d?d?d?d?d",
|
||||
"79219?d?d?d?d?d?d",
|
||||
"793110?d?d?d?d?d",
|
||||
"793111?d?d?d?d?d",
|
||||
"793112?d?d?d?d?d",
|
||||
"793113?d?d?d?d?d",
|
||||
"79312?d?d?d?d?d?d",
|
||||
"79313?d?d?d?d?d?d",
|
||||
"79315?d?d?d?d?d?d",
|
||||
"79319?d?d?d?d?d?d",
|
||||
"79291?d?d?d?d?d?d",
|
||||
"799923?d?d?d?d?d",
|
||||
"799924?d?d?d?d?d",
|
||||
"799925?d?d?d?d?d",
|
||||
"7963241?d?d?d?d",
|
||||
"7963242?d?d?d?d",
|
||||
"7963243?d?d?d?d",
|
||||
"7963244?d?d?d?d",
|
||||
"7963245?d?d?d?d",
|
||||
"7963246?d?d?d?d",
|
||||
"7963247?d?d?d?d",
|
||||
"7963248?d?d?d?d",
|
||||
"7963249?d?d?d?d",
|
||||
"796330?d?d?d?d?d",
|
||||
"796331?d?d?d?d?d",
|
||||
"796332?d?d?d?d?d",
|
||||
"796334?d?d?d?d?d",
|
||||
"79650?d?d?d?d?d?d",
|
||||
"79651?d?d?d?d?d?d",
|
||||
"79652?d?d?d?d?d?d",
|
||||
"79653?d?d?d?d?d?d",
|
||||
"79654?d?d?d?d?d?d",
|
||||
"79655?d?d?d?d?d?d",
|
||||
"79656?d?d?d?d?d?d",
|
||||
"79657?d?d?d?d?d?d",
|
||||
"79658?d?d?d?d?d?d",
|
||||
"79659?d?d?d?d?d?d",
|
||||
"790520?d?d?d?d?d",
|
||||
"790521?d?d?d?d?d",
|
||||
"790522?d?d?d?d?d",
|
||||
"790523?d?d?d?d?d",
|
||||
"790524?d?d?d?d?d",
|
||||
"790525?d?d?d?d?d",
|
||||
"790526?d?d?d?d?d",
|
||||
"790527?d?d?d?d?d",
|
||||
"790528?d?d?d?d?d",
|
||||
"790624?d?d?d?d?d",
|
||||
"790625?d?d?d?d?d",
|
||||
"790626?d?d?d?d?d",
|
||||
"790627?d?d?d?d?d",
|
||||
"790958?d?d?d?d?d",
|
||||
"790959?d?d?d?d?d",
|
||||
"796023?d?d?d?d?d",
|
||||
"796024?d?d?d?d?d",
|
||||
"796025?d?d?d?d?d",
|
||||
"796026?d?d?d?d?d",
|
||||
"796027?d?d?d?d?d",
|
||||
"796028?d?d?d?d?d",
|
||||
"796434?d?d?d?d?d",
|
||||
"79500?d?d?d?d?d?d",
|
||||
"79502?d?d?d?d?d?d",
|
||||
"79516?d?d?d?d?d?d",
|
||||
"79517?d?d?d?d?d?d",
|
||||
"79518?d?d?d?d?d?d",
|
||||
"79522?d?d?d?d?d?d",
|
||||
"79523?d?d?d?d?d?d",
|
||||
"79531?d?d?d?d?d?d",
|
||||
"79533?d?d?d?d?d?d",
|
||||
"790433?d?d?d?d?d",
|
||||
"790451?d?d?d?d?d",
|
||||
"790455?d?d?d?d?d",
|
||||
"790460?d?d?d?d?d",
|
||||
"790461?d?d?d?d?d",
|
||||
"790462?d?d?d?d?d",
|
||||
"790463?d?d?d?d?d",
|
||||
"790464?d?d?d?d?d",
|
||||
"799100?d?d?d?d?d",
|
||||
"799101?d?d?d?d?d",
|
||||
"799102?d?d?d?d?d",
|
||||
"790062?d?d?d?d?d",
|
||||
"790063?d?d?d?d?d",
|
||||
"790064?d?d?d?d?d",
|
||||
"790065?d?d?d?d?d",
|
||||
"799900?d?d?d?d?d",
|
||||
"799901?d?d?d?d?d",
|
||||
"799902?d?d?d?d?d",
|
||||
"799903?d?d?d?d?d",
|
||||
"799904?d?d?d?d?d",
|
||||
"799909?d?d?d?d?d",
|
||||
"79991?d?d?d?d?d?d",
|
||||
"79992?d?d?d?d?d?d",
|
||||
"799322?d?d?d?d?d",
|
||||
"799343?d?d?d?d?d",
|
||||
"799344?d?d?d?d?d",
|
||||
"799555?d?d?d?d?d",
|
||||
"799559?d?d?d?d?d",
|
||||
"799560?d?d?d?d?d",
|
||||
"799590?d?d?d?d?d",
|
||||
"795840?d?d?d?d?d",
|
||||
"795856?d?d?d?d?d",
|
||||
"799511?d?d?d?d?d",
|
||||
"799512?d?d?d?d?d",
|
||||
"7999944?d?d?d?d",
|
||||
"7999945?d?d?d?d",
|
||||
"7999946?d?d?d?d",
|
||||
"7999947?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_spb_9",
|
||||
name="MobileOperSpb_9",
|
||||
description="Перебор мобильных номеров Санкт-Петербурга без 7 и знака +.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"91100?d?d?d?d?d",
|
||||
"91101?d?d?d?d?d",
|
||||
"91102?d?d?d?d?d",
|
||||
"91103?d?d?d?d?d",
|
||||
"91108?d?d?d?d?d",
|
||||
"91109?d?d?d?d?d",
|
||||
"91110?d?d?d?d?d",
|
||||
"91111?d?d?d?d?d",
|
||||
"91112?d?d?d?d?d",
|
||||
"91117?d?d?d?d?d",
|
||||
"91119?d?d?d?d?d",
|
||||
"9811?d?d?d?d?d?d",
|
||||
"9817?d?d?d?d?d?d",
|
||||
"9818?d?d?d?d?d?d",
|
||||
"9819?d?d?d?d?d?d",
|
||||
"98986?d?d?d?d?d",
|
||||
"98686?d?d?d?d?d",
|
||||
"98687?d?d?d?d?d",
|
||||
"98688?d?d?d?d?d",
|
||||
"92118?d?d?d?d?d",
|
||||
"92119?d?d?d?d?d",
|
||||
"9213?d?d?d?d?d?d",
|
||||
"9214?d?d?d?d?d?d",
|
||||
"9215?d?d?d?d?d?d",
|
||||
"9216?d?d?d?d?d?d",
|
||||
"9217?d?d?d?d?d?d",
|
||||
"9218?d?d?d?d?d?d",
|
||||
"9219?d?d?d?d?d?d",
|
||||
"93110?d?d?d?d?d",
|
||||
"93111?d?d?d?d?d",
|
||||
"93112?d?d?d?d?d",
|
||||
"93113?d?d?d?d?d",
|
||||
"9312?d?d?d?d?d?d",
|
||||
"9313?d?d?d?d?d?d",
|
||||
"9315?d?d?d?d?d?d",
|
||||
"9319?d?d?d?d?d?d",
|
||||
"9291?d?d?d?d?d?d",
|
||||
"99923?d?d?d?d?d",
|
||||
"99924?d?d?d?d?d",
|
||||
"99925?d?d?d?d?d",
|
||||
"963241?d?d?d?d",
|
||||
"963242?d?d?d?d",
|
||||
"963243?d?d?d?d",
|
||||
"963244?d?d?d?d",
|
||||
"963245?d?d?d?d",
|
||||
"963246?d?d?d?d",
|
||||
"963247?d?d?d?d",
|
||||
"963248?d?d?d?d",
|
||||
"963249?d?d?d?d",
|
||||
"96330?d?d?d?d?d",
|
||||
"96331?d?d?d?d?d",
|
||||
"96332?d?d?d?d?d",
|
||||
"96334?d?d?d?d?d",
|
||||
"9650?d?d?d?d?d?d",
|
||||
"9651?d?d?d?d?d?d",
|
||||
"9652?d?d?d?d?d?d",
|
||||
"9653?d?d?d?d?d?d",
|
||||
"9654?d?d?d?d?d?d",
|
||||
"9655?d?d?d?d?d?d",
|
||||
"9656?d?d?d?d?d?d",
|
||||
"9657?d?d?d?d?d?d",
|
||||
"9658?d?d?d?d?d?d",
|
||||
"9659?d?d?d?d?d?d",
|
||||
"90520?d?d?d?d?d",
|
||||
"90521?d?d?d?d?d",
|
||||
"90522?d?d?d?d?d",
|
||||
"90523?d?d?d?d?d",
|
||||
"90524?d?d?d?d?d",
|
||||
"90525?d?d?d?d?d",
|
||||
"90526?d?d?d?d?d",
|
||||
"90527?d?d?d?d?d",
|
||||
"90528?d?d?d?d?d",
|
||||
"90624?d?d?d?d?d",
|
||||
"90625?d?d?d?d?d",
|
||||
"90626?d?d?d?d?d",
|
||||
"90627?d?d?d?d?d",
|
||||
"90958?d?d?d?d?d",
|
||||
"90959?d?d?d?d?d",
|
||||
"96023?d?d?d?d?d",
|
||||
"96024?d?d?d?d?d",
|
||||
"96025?d?d?d?d?d",
|
||||
"96026?d?d?d?d?d",
|
||||
"96027?d?d?d?d?d",
|
||||
"96028?d?d?d?d?d",
|
||||
"96434?d?d?d?d?d",
|
||||
"9500?d?d?d?d?d?d",
|
||||
"9502?d?d?d?d?d?d",
|
||||
"9516?d?d?d?d?d?d",
|
||||
"9517?d?d?d?d?d?d",
|
||||
"9518?d?d?d?d?d?d",
|
||||
"9522?d?d?d?d?d?d",
|
||||
"9523?d?d?d?d?d?d",
|
||||
"9531?d?d?d?d?d?d",
|
||||
"9533?d?d?d?d?d?d",
|
||||
"90433?d?d?d?d?d",
|
||||
"90451?d?d?d?d?d",
|
||||
"90455?d?d?d?d?d",
|
||||
"90460?d?d?d?d?d",
|
||||
"90461?d?d?d?d?d",
|
||||
"90462?d?d?d?d?d",
|
||||
"90463?d?d?d?d?d",
|
||||
"90464?d?d?d?d?d",
|
||||
"99100?d?d?d?d?d",
|
||||
"99101?d?d?d?d?d",
|
||||
"99102?d?d?d?d?d",
|
||||
"90062?d?d?d?d?d",
|
||||
"90063?d?d?d?d?d",
|
||||
"90064?d?d?d?d?d",
|
||||
"90065?d?d?d?d?d",
|
||||
"99900?d?d?d?d?d",
|
||||
"99901?d?d?d?d?d",
|
||||
"99902?d?d?d?d?d",
|
||||
"99903?d?d?d?d?d",
|
||||
"99904?d?d?d?d?d",
|
||||
"99909?d?d?d?d?d",
|
||||
"9991?d?d?d?d?d?d",
|
||||
"9992?d?d?d?d?d?d",
|
||||
"99322?d?d?d?d?d",
|
||||
"99343?d?d?d?d?d",
|
||||
"99344?d?d?d?d?d",
|
||||
"99555?d?d?d?d?d",
|
||||
"99559?d?d?d?d?d",
|
||||
"99560?d?d?d?d?d",
|
||||
"99590?d?d?d?d?d",
|
||||
"95840?d?d?d?d?d",
|
||||
"95856?d?d?d?d?d",
|
||||
"99511?d?d?d?d?d",
|
||||
"99512?d?d?d?d?d",
|
||||
"999944?d?d?d?d",
|
||||
"999945?d?d?d?d",
|
||||
"999946?d?d?d?d",
|
||||
"999947?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_plus_79",
|
||||
name="MobileOper+79",
|
||||
description="Перебор мобильных номеров с префиксом +79.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"+7911?d?d?d?d?d?d?d",
|
||||
"+7904?d?d?d?d?d?d?d",
|
||||
"+7931?d?d?d?d?d?d?d",
|
||||
"+7921?d?d?d?d?d?d?d",
|
||||
"+7999?d?d?d?d?d?d?d",
|
||||
"+7905?d?d?d?d?d?d?d",
|
||||
"+7951?d?d?d?d?d?d?d",
|
||||
"+7952?d?d?d?d?d?d?d",
|
||||
"+7953?d?d?d?d?d?d?d",
|
||||
"+7901?d?d?d?d?d?d?d",
|
||||
"+7903?d?d?d?d?d?d?d",
|
||||
"+7950?d?d?d?d?d?d?d",
|
||||
"+7906?d?d?d?d?d?d?d",
|
||||
"+7909?d?d?d?d?d?d?d",
|
||||
"+7929?d?d?d?d?d?d?d",
|
||||
"+7960?d?d?d?d?d?d?d",
|
||||
"+7961?d?d?d?d?d?d?d",
|
||||
"+7962?d?d?d?d?d?d?d",
|
||||
"+7963?d?d?d?d?d?d?d",
|
||||
"+7965?d?d?d?d?d?d?d",
|
||||
"+7966?d?d?d?d?d?d?d",
|
||||
"+7981?d?d?d?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_89",
|
||||
name="MobileOper_89",
|
||||
description="Перебор мобильных номеров с префиксом 89.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"8911?d?d?d?d?d?d?d",
|
||||
"8904?d?d?d?d?d?d?d",
|
||||
"8931?d?d?d?d?d?d?d",
|
||||
"8921?d?d?d?d?d?d?d",
|
||||
"8999?d?d?d?d?d?d?d",
|
||||
"8905?d?d?d?d?d?d?d",
|
||||
"8951?d?d?d?d?d?d?d",
|
||||
"8952?d?d?d?d?d?d?d",
|
||||
"8953?d?d?d?d?d?d?d",
|
||||
"8901?d?d?d?d?d?d?d",
|
||||
"8903?d?d?d?d?d?d?d",
|
||||
"8950?d?d?d?d?d?d?d",
|
||||
"8906?d?d?d?d?d?d?d",
|
||||
"8909?d?d?d?d?d?d?d",
|
||||
"8929?d?d?d?d?d?d?d",
|
||||
"8960?d?d?d?d?d?d?d",
|
||||
"8961?d?d?d?d?d?d?d",
|
||||
"8962?d?d?d?d?d?d?d",
|
||||
"8963?d?d?d?d?d?d?d",
|
||||
"8965?d?d?d?d?d?d?d",
|
||||
"8966?d?d?d?d?d?d?d",
|
||||
"8981?d?d?d?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_79",
|
||||
name="MobileOper_79",
|
||||
description="Перебор мобильных номеров без знака +.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"7911?d?d?d?d?d?d?d",
|
||||
"7904?d?d?d?d?d?d?d",
|
||||
"7931?d?d?d?d?d?d?d",
|
||||
"7921?d?d?d?d?d?d?d",
|
||||
"7999?d?d?d?d?d?d?d",
|
||||
"7905?d?d?d?d?d?d?d",
|
||||
"7951?d?d?d?d?d?d?d",
|
||||
"7952?d?d?d?d?d?d?d",
|
||||
"7953?d?d?d?d?d?d?d",
|
||||
"7901?d?d?d?d?d?d?d",
|
||||
"7903?d?d?d?d?d?d?d",
|
||||
"7950?d?d?d?d?d?d?d",
|
||||
"7906?d?d?d?d?d?d?d",
|
||||
"7909?d?d?d?d?d?d?d",
|
||||
"7929?d?d?d?d?d?d?d",
|
||||
"7960?d?d?d?d?d?d?d",
|
||||
"7961?d?d?d?d?d?d?d",
|
||||
"7962?d?d?d?d?d?d?d",
|
||||
"7963?d?d?d?d?d?d?d",
|
||||
"7965?d?d?d?d?d?d?d",
|
||||
"7966?d?d?d?d?d?d?d",
|
||||
"7981?d?d?d?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mobile_oper_9",
|
||||
name="MobileOper_9",
|
||||
description="Перебор мобильных номеров без 7 и знака +.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": f"mask-{index:03d}",
|
||||
"attack_mode": 3,
|
||||
"mask": mask,
|
||||
}
|
||||
for index, mask in enumerate(
|
||||
(
|
||||
"911?d?d?d?d?d?d?d",
|
||||
"904?d?d?d?d?d?d?d",
|
||||
"931?d?d?d?d?d?d?d",
|
||||
"921?d?d?d?d?d?d?d",
|
||||
"999?d?d?d?d?d?d?d",
|
||||
"905?d?d?d?d?d?d?d",
|
||||
"951?d?d?d?d?d?d?d",
|
||||
"952?d?d?d?d?d?d?d",
|
||||
"953?d?d?d?d?d?d?d",
|
||||
"901?d?d?d?d?d?d?d",
|
||||
"903?d?d?d?d?d?d?d",
|
||||
"950?d?d?d?d?d?d?d",
|
||||
"906?d?d?d?d?d?d?d",
|
||||
"909?d?d?d?d?d?d?d",
|
||||
"929?d?d?d?d?d?d?d",
|
||||
"960?d?d?d?d?d?d?d",
|
||||
"961?d?d?d?d?d?d?d",
|
||||
"962?d?d?d?d?d?d?d",
|
||||
"963?d?d?d?d?d?d?d",
|
||||
"965?d?d?d?d?d?d?d",
|
||||
"966?d?d?d?d?d?d?d",
|
||||
"981?d?d?d?d?d?d?d",
|
||||
),
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="mask_8_digits",
|
||||
name="8-digital",
|
||||
description="Перебор восьмизначных числовых паролей.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "mask-001",
|
||||
"attack_mode": 3,
|
||||
"mask": "?d?d?d?d?d?d?d?d",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="breachcompilation",
|
||||
name="BreachCompilation",
|
||||
description="Перебор по словарю BreachCompilation.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "dictionary-001",
|
||||
"attack_mode": 0,
|
||||
"dictionary": {
|
||||
"resource": "wordlist:breachcompilation",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
StockMethod(
|
||||
method_id="insidepro_wpa",
|
||||
name="insidePRO_WPA",
|
||||
description="Перебор по словарю insidePRO_WPA.",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"hash_mode": 22000,
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "dictionary-001",
|
||||
"attack_mode": 0,
|
||||
"dictionary": {
|
||||
"resource": "wordlist:insidepro-wpa",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class StockMethodSeeder:
|
||||
def __init__(self, conn: sqlite3.Connection) -> None:
|
||||
self._conn = conn
|
||||
self._methods = MethodService(conn)
|
||||
|
||||
def seed(self) -> None:
|
||||
with self._conn:
|
||||
for stock_method in STOCK_METHODS:
|
||||
if stock_method.definition is None:
|
||||
continue
|
||||
|
||||
existing_method = self._methods.get_method(
|
||||
stock_method.method_id
|
||||
)
|
||||
|
||||
if existing_method is None:
|
||||
self._methods.create_method(
|
||||
method_id=stock_method.method_id,
|
||||
name=stock_method.name,
|
||||
description=stock_method.description,
|
||||
)
|
||||
else:
|
||||
if existing_method["name"] != stock_method.name:
|
||||
raise ValueError(
|
||||
"Stock method ID is already used with a "
|
||||
f"different name: {stock_method.method_id}"
|
||||
)
|
||||
|
||||
existing_version = self._methods.get_method_version(
|
||||
stock_method.method_id,
|
||||
1,
|
||||
)
|
||||
|
||||
if existing_version is None:
|
||||
self._methods.create_method_version(
|
||||
method_id=stock_method.method_id,
|
||||
version=1,
|
||||
definition=stock_method.definition,
|
||||
)
|
||||
elif existing_version["definition_json"] != (
|
||||
self._canonical_json(stock_method.definition)
|
||||
):
|
||||
raise ValueError(
|
||||
"Stock method version 1 already exists with "
|
||||
f"different definition: {stock_method.method_id}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _canonical_json(definition: dict[str, Any]) -> str:
|
||||
import json
|
||||
|
||||
return json.dumps(
|
||||
definition,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
)
|
||||
Reference in New Issue
Block a user